Отчёт команды THIRTEEN

ICFP Programming Contest

2014-07-28

Введение

Отчёт от tilarids

Отчёт от san

Отчёт от brox

Про bkg.png

Введение

Пропустить ICFPC-2014 команда THIRTEEN никак не могла, и поэтому 25 июля 2014 года в 12:00 UTC (а у некоторых было 5 утра!) собралась почти тем же составом, что и в прошлом году. В этот раз собраться всем вместе в том же месте не получилось, в команде впервые появились очень удалённые участники. Но основной состав всё же смог собраться в офисе у Brox.
Впрочем, это мы немного забегаем вперёд, потому что контест для THIRTEEN начался еще за неделю до этого. С того, что Brox обнаружил некоторые нерегулярности в чёрной фоновой картинке с сайта…

Отчёт от tilarids

Так вышло, что поучаствовать в ICFPC в этом году вживую у меня никак не получалось. Но это не повод отчаиваться! Хоть Сан Франциско и находится за 10 часовых поясов от Харькова, ICFPC - это не то, что стоит пропускать. Также был положительный опыт “Зимнего ICFPC”, в котором вообще вся координация происходила по скайпу.

Чтобы хоть как-то сгладить негативный эффект от удалённости, я решил сразу писать весь код на Java. Как показывает практика, это Lingua Franca для нашей команды (а еще это язык, на котором пишет Sanny, да еще и обычно пишет очень много кода :)).

Решение это было одновременно и правильным, и не очень. Правильность в том, что таки удалось реиспользовать мой код для решения задачи. Неправильность в том, что Java я так и не знаю, да и инструментарий не был подготовлен. Поэтому я за время контеста перешёл с “Eclipse + OpenJDK1.7” на “Sublime Text 3 + OracleJDK1.8 + javac src/**/*.java”, а заканчивал вообще с “IDEA + OracleJDK1.8”. Еще я узнал, что в Java Stack обходится в неочевидном порядке, а сравнение двух Integer’ов - весьма опасная штука :)

По прочтению задания ранним утром мгновенно стало ясно, что нужно писать компилятор с какого-либо ЯВУ в коды SECD процессора (о том, что эта штука называется SECD мы узнали уже по окончании контеста). И в то время как Харьков решил писать DSL на Java, поставив во главу возможность запускать и отлаживать программу на хостовом языке, я решил попробовать написать компилятор JavaScript. Причина проста - я хотел в качестве хостового языка нечто похожее на целевую модель, я хотел иметь возможность экспериментировать с этим. И JavaScript - это, наверное, простейший язык с замыканиями и без скобочек, который я знаю. Сейчас я думаю, что стоило перебороть себя и попробовать нечто со скобочками. Потому что писать всё равно приходилось в стиле, который для JavaScript не свойственен. Еще более правильным решением было бы не писать компилятор вообще. Я понимал, что в Харькове пишут свой, но остановиться перед такой интересной задачей я не мог. 

Итак, написание компилятора я начал борьбой с ANTLR3, после чего довольно быстро завёл парсер на ANTLR4. Еще пара часов - и был готов простейший компилятор. Заняло это от публикации задания до первой версии, которая позволяла скомпилировать простейшего бота, часов 7, из которых часа полтора было потрачено на дорогу и daytime job (пятница - рабочий день же!). Я тут же попытался этот компилер “продать” команде в Харькове в надежде, что кто-то пожелает писать на JavaScript, а не на Java-based DSL. Покупать никто не желал, и тут бы мне напрячься, тем более что

[T+8h] Sanny: пытаюсь нагнуть людей писать на полу-жабе, т.к. уже надо писать...
[T+8h] Sanny: тиларидз бери жабу и пеши уже сразу

И я даже посмотрел на то, что умеет компилятор за авторством Sanny. Но у меня еще была куча идей, что можно сделать с моим компилятором. Так, я закончил реализацию всей арифметики, добавил мутабельные локальные переменные, циклы, так что можно было писать весьма императивный код. В качестве последней крутой фичи я добавил динамическую память - адресуемые массивы. Сделать это было не так просто, потому что команды чтения из фрейма и записи в фрейм не принимали индекс в качестве параметров. Т.е., можно было попросить прочитать 3-ю ячейку и записать 5-ю, а вот попросить прочитать i-ю ячейку было нельзя. Для решения этой проблемы я генерировал подобный код (почему-то я решил назвать это виртуальными таблицами):

- А не является ли i нулём? Нет? Двигаем дальше.
- А не является ли i единичкой? Нет? Двигаем дальше.

- А не является ли i двоечкой? Да? LD 0 2!

Эти таблицы генерировались единожды и впоследствии вызывались посредством local-call (sel/join). Всё это заработало в T+18h. В результате можно было не задумываясь писать такое:

function step(state, world, n, m, lm_status) {
 var i;
 var it1;
 var row;
 var cell;
 var lm_x;
 var lm_y;

 lm_y = car(car(cdr(lm_status)))
 lm_x = cdr(car(cdr(lm_status)))
 i = 0
 it1 = world
 while (n > i) {
   row = car(it1)
   //...

}

function step_impl(state, tuple4) {
 return init_memory(step, // func to call
                    200, // mem size
                    state,
                    car(tuple4),
                    length(car(tuple4)) - 1,
                    length(car(car(tuple4))) - 1,
                    car(cdr(tuple4)))
}

function init() {

  return [0, step_impl];

}

Доступ к памяти осуществлялся посредством вызовов функций read_memory(i) и write_memory(i, value). В общем и целом - сплошная императивщина. К сожалению, даже это не сподвигло тиммейтов в Харькове писать алгоритмы на JavaScript. Впрочем, это и хорошо, активное использование read_memory/write_memory явно бы привело к нехватке количества инструкций. В свою очередь я честно попробовал сам написать что-либо на JS, но через несколько часов сдался. Здесь впервые появилась самая страшная напасть этого ICFPC: писать компиляторы в разы интересней, чем скучный AI для простой клеточной игры. После многочасового фана по написанию компилятора для лиспового процессора волновые алгоритмы не доставляли совершенно. Так что я сделал правильную вещь - на [T+21h] я окончательно забросил свой компилятор и больше к нему не притрагивался до конца контеста. На lightning ушёл submission, которым занимался Sanny. А я, прочитав задание для основного раунда, отключился на 4 часа.

Проснувшись, и разобравшись, что же умеет наш DSL, я довольно быстро набросал реализацию immutable map(как оказалось впоследствии - довольно бажную реализацию, но особых проблем это не вызвало). После этого реализовал процессор GHC на GCC, что давало возможность эмулировать поведение привидений внутри лямбдабота.

На этом этапе ([T+37h]) наш бот уже активно бегает по карте, выедает таблетки, и даже умудряется запускать волну на тот случай, если ему не хватает инфы о ближайших рёбрах (эта инфа считается единожды при инициализации). К сожалению, он довольно медленный, в основном потому, что вовсю использует списки и линейный доступ к ним. Переписываю всю алгоритмику на immutable maps, с O(log2 N) доступом - [T+40h]. И после длительной войны Sanny с компилятором получаю результат ([T+44h]) - никакого видимого прироста производительности нет! Видимо, реализация immutable maps настолько громоздка, что log2N сравним с N при небольших N. Вот это “видимо” нужно было подтверждать числами, а не сравнивать на глаз. Очевидно, что immutable maps нужно было доводить до конца.

До [T+49h] занимался внутренней логикой бота на коротких расстояниях (этот код был впоследствии удалён и так никуда и не попал). А потом за 2.5 часа сделал то, что нужно было сделать в самом-самом начале - написал наш внутренний эмулятор GCC. Если бы это было сделано раньше, отладка компилятора сильно бы упростилась. Обоих компиляторов. Проверив, что простейшие программы в эмуляторе работают, уснул.

Проснувшись, увидел, что Sanny добавил в эмулятор счётчик инструкций, отладил его и активно использует. Порадовался, и прикрутил к эмулятору ABI. Т.е., добавил возможность писать такое:

GCCEmulator cpu = new GCCEmulator("test.txt", 2);

cpu.storeInFrame(0, worldState);

И это через Java reflection конвертировалось в правильные внутренние структуры. Этот же worldState использовался как есть в алгоритме, работал как обычный класс при запуске Java кода. И при компиляции переводился уже в CONS списки.

Последние несколько часов пытался реализовать альтернативное AI для бота, используя A* для поиска пути. A* работал плохо, и на доводку его времени не оставалось. Сабмитили то, что было + привидений, к написанию которых я даже и не притрагивался.

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

Отчёт от san

TLDR: Я компилил Java в байткод лямбда-мена с помощью ECJ + руки, и еще мне помогали кто чем может по мелочам. Затем коллеги писали ghost-ов на простом ассемблере, RST накомпилял с языка Ц (?), а мы с tilarids писали на Java лямбда мена, безуспешно, потому что мутабельность придумали поздно, а алгоритм наш был с использованием “дуг” от развилки до развилки (граф) , что работало быстро, но писалось сложно, и не взлетело совсем. Еще tilarids написал эмулятор ЦПУ, прикрутил к нему мою эмуляцию мира с рандомными ботами, и мы могли тестить компилятор и профилировать программу на перфоманс. Java с лямбдами рулит!

Теперь подробнее.

Я собрался в месте проведения контеста за 40 минут до начала, и пока настроился и то да сё, уже открыли задание, и мы все начали его читать. Задание было большое и обстоятельное. Я его пробежался и понял общую суть, что pacman и лисп-машина, а детали думал почитать потом (оказалось, что и потом не почитал, а спрашивал у коллег по команде, чтобы быстрее). Так как стало ясно, что прямо на языке указанном в спецификации, его писать не получится, пришлось решать на чём писать. Подумав с полчасика, мы пошли в кафешку всей толпой и стали думать там. Думали еще часа полтора и вышло у нас понимание, что как ни крути, высокоуровневый язык будет похож на лисп, и что хочется не просто “написал, откомпилировал, прогнал в предлагаемом эмуляторе, почитал trace”, а чего-то большего, например отладку в IDE.  

Я в курсе, что существуют споры “написал, поставил брейкпойнт,остановился на нем, подумал“ VS “подумал, написал, запустил, заработало.”, так вот я отношусь к первой категории и еще с детства инвалид по JAVA и поэтому понял, что будем компилить java. И java эта будет с лямбдами. То что она будет еще с женериками, выяснилось позже, в процессе написания.

Tilarids вбросил по скайпу в кафешку, что ANTLR для Java есть, но я нашел что он без лямбд. Чтобы таки взлететь, оставалось произвести последнее действие, о котором я до того только мог мечтать - взять Eclipse Compiler for Java, как это делают все люди, и заюзать его. В нем есть офигенный парсер, который возвращает офигенный DOM, который описывает и лямбды и всё-всё-всё.

Когда пришли из кафешки, оказалось, что сам Tilarids решил парсить Javascript, но так как я указанный язык не одобряю ни с какой стороны, кроме разве что target platform для компиляторов, потому что он не типизированный, то решил, что будущее покажет.

Короче, как-то так оказалось, что за педалинг я сел то ли в +4 то ли в +5 часов от начала, а до этого в голове у меня плодились идеи компилятора. Очень быстро я определил подмножество Java, на котором уже можно было писать программу, и в этот момент уже можно было начинать писать всё что угодно, покуда я доделываю компилятор, но пишущие коллеги уже озадачили себя ghost-ами, а думающие не были сильны в функциональных алгоритмах, а особенно в жабовом исполнении (nonsense!).

Все мои первые сутки прошли в итерационном улучшении компилятора, ловле вредного бага (от -7 до -2 до lightning deadline) и написании первой программы бота (от -2 до 0), которая парсила на этапе инициализации карту на дуги графа через BFS, и потом в первой по близости от бота дуге считала точки и шла туда, где больше дают. Если точек нет, она стояла. Эту программу мы и засабмитили, она набирала 1400 очков, а jabber.ru между прочим, похвастался, что у него уже 10К....

Далее я совершил 3 фатальные ошибки.

1) Сел писать алгоритм сам, а не в паре с товарищем

2) Не додумал сначала как сделать мутабельность, потому что не вкурил как работают CLOSURES при возврате лямбд из функции, а tilarids меня научил токо в середине 2 дня, когда уже дофига кода была написано, который выкидывать жалко.

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

По второму вопросу: SAS регулярно поднимал тему массивов с O(1) на основе команды записи в environment (в java-script смысле), которая называлась ST и которую я использовал для записи локальных переменных функции. Я это отрицал по двум причинам. Во-первых, прямой доступ даже к имеющемуся массиву по произвольному индексу потребует log2(N) сравнений и прыжков, покуда не допрыгает до нужного места, и это не O(1), а во-вторых я не понимал как менеджить этот массив, т.к. не вкурил этих CLOSURES, их отношений и их lifecycle (т.е. оставил непонятой часть спецификации, аяяй мне!). После объяснений тиларидза о том, что ссылка на CLOSURE сохраняется в момент LDF, у меня мозг стал на место, и я внезапно понял как сделать мутабельный массив, и как его заюзать чтобы не прыгать по сравнениям: для этого всю дугу от перекрестка до перекрестка нужно проинициализировать лямбдами, сразу читающими нужый элемент (ссылки на команды LD const_env const_addr), и проход по дуге превратится в прямой доступ к соответствующим ячейкам заранее проинициализированного картой массива. В конечном итоге я написал этот массив на лямбда-ассемблере, прикрутил ассемблер в компилятор и сделал соответствующую java implementation, чтобы можно было юзать и в прямой java. Ну и постепенно начал перегонять в это дело код (в чём мне помогал SVG), сильно (раз в 6, а может и более) ускорив его, но не перевёл всё до конца, оставив некоторые части как были. Постепенно я дошел и до массива размером в 1, и открывающиеся упущенные перспективы очень грузили меня, ибо время было на исходе.

Короче, вот так остальные 2 дня прошли в переменном улучшении компилятора и набрасывании нового лямбдо-кода на компилятор. Постоянное ощущение что вот исправлю последний баг компилятора и всё сразу заработает (так и происходило в общей сложности раз 5-6,, и облегчение наступало, пока в моем компилируемом коде не всплывал очередной case, но ко времени +2.5days всё наконец устаканилось). Вдохновляло, что tilarids мог на этой жабе писать сложные алгоритмы, например эмулятор программ ghosts, который мы так и не заюзали (негде!)

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

Короче, реальным позитивным результатом своей работы считаю

1) очередную дозу фана

2) знакомство с очень классным ECJ, который просто взял и заработал сразу.

3) написание с его помощью компилятора таких вкусностей (которые тупой голове не помогли впрочем):


@Compiled

static class WorldState extends Cons {

   ListCons<ListCons<Integer>> map;

   LambdaManState lambdaManState;

   ListCons<GhostState> ghosts;

   int fruitStatus;

   WorldState(ListCons<ListCons<Integer>> map, LambdaManState lambdaManState,

                    ListCons<GhostState> ghosts, int fruitStatus) {

       super(null, null);

       this.map = map;

       this.lambdaManState = lambdaManState;

       this.ghosts = ghosts;

       this.fruitStatus = fruitStatus;

   }

}

@Compiled

private Tuple<AIState, Function2<AIState, WorldState, Tuple<AIState, Integer>>>

               entryFactual(WorldState ws, ListCons<ListCons> ghostSpecs) {

   AIState initialState = createInitialState(ws, ghostSpecs);

   return new Tuple<>(initialState,

           (nextaistate, worldState) -> performMove(nextaistate, worldState));

}

И всякие

ListCons<ListCons<ParsedEdge>> newRoutes =

            map(following, (ParsedEdge next) -> cons(next, lookingEdge));

Queue<ListCons<ParsedEdge>> newqq =

      fold0(newRoutes,

            reduced.b,

            (Queue<ListCons<ParsedEdge>> qq, ListCons<ParsedEdge> nr) ->

                  queue_enqueue(qq, nr));

Здесь нужно обратить внимание и на то, что WorldState реально описывает структуру данных, которую по спецификации получает наша entryPoint, и все остальные типы тоже соответствуют. Java тестировала все типы на компилируемость, а мой компилятор уже компилировал просто текст, используя типы даже без женериков (лишь кое где в сложных выражениях приходилось аннотировать (добавлять class cast) типы напрямую).

Отчёт от brox

Так уж вышло, что задание главного раунда чудесным образом делилось на 2 независимые части. Поскольку над лямбда-ботом вовсю пыхтели sanny & tilarids, логично было мне и rst7 заняться привидениями. Почему логично? Для rst микроконтроллеры и ассемблер - повседневная работа, а для меня - старые добрые времена. Сперва ZX Spectrum, потом заполонившие все советские журналы самодельные компы на 580-й серии, потом Commodore 64, демосцена и, наконец, Hugi code optimization compo.

В сравнении с SECD процессором лямбда-бота, наш проц выглядел просто чудом совершенства - необходимый минимум инструкций, отличная модель памяти с разнообразной адресацией. Потратив совсем немного времени, мы быстро изобрели колесо стек вместе со всеми сопутствующими плюшками - call, ret, push, pop. Сделали символьные метки (а также полезные прибамбасы вроде jmp $+2). Полноценный препроцессор/ассемблер сделал для нас svg на джаве.

Это хорошая часть. В глубокий пессимизм ввергал тот факт, что у нашего целевого процессора 256 байт памяти, что ставило жирный крест на всех умных алгоритмах поиска пути. Осознав это, мы начали выдавать на-гора всевозможные глупые алгоритмы. Базовых идей было 2.

1. Бот-патруль. Мы быстро сварганили бота, который умел ходить по правилу правой/левой руки вокруг замкнутой области. На это уходит всего 7 инструкций процессора:

int 3

int 6

mov A, B

inc A

and A, 3

int 0

hlt

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

2. Бот-охотник. Два варианта этого мы с rst и подготовили к завершению. Я начал с того, что просто вычислял наилучшее направление на лямбда-бота по разнице координат (по квадрантам). Менял направления только на перекрестках. И если наилучшее направление оказывалось недоступным (стена или запрещенный разворот назад), то выбирал лучшее из оставшихся. Этот алгоритм работал вполне неплохо и съедал 3 жизни нашего лямбда бота, давая тому прожить 20000 тактов и за это время набрать порядка 1000 очков. Ближе к завершению третьего дня svg подбросил идею “целиться с упреждением”. То есть мы запоминали старое положение лямбда-бота, читали его новую позицию и целились вдоль его вектора движения. Величину упреждения я потьюнил на наших собственных ботах, имевшихся на тот момент. Прослеживался четкий экстремум, если целить на 3 шага вперед. Оный ghost и был засабмичен. А также туда добавили элемент “грязной игры” - в промежутках между перекрестками я запускал генератор случайных чисел, и обращался к случайной ячейке карты. И все это с особым цинизмом зациклено - выход по исчерпанию лимита в 1024 инструкции. Не знаю, были ли такие команды, кто сделал полноценную эмуляцию GHC на GCC, но мы постарались осложнить им жизнь, как могли.

Про bkg.png

За 5 дней до начала соревнования brox нашел, что в bkg.png (картинке которая использовалась для фона на странице-объявлении об ICFPC) закодирована секретная информация (картинка ниже). Если ее сделать более видимой, то получается такая картинка (обратить внимание на бордюр толщиной в 1 пиксель).

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