Предлагаю вашему вниманию перевод доклада Роберта Конрада с прошедшего в октябре прошлого года HaxeUp Sessions 2019 Linz. Данный доклад посвящен процессу портирования на консоли игры CrossCode, изначально написанной на HTML5. Если вам понравится перевод, то рекомендую также посмотреть его в оригинале на английском — Роберт очень веселый человек, постоянно шутит и его интересно слушать.  Немного об авторе: Роберт работает программистом в Deck13, одной из немногих игровых компаний, использующих свои собственные технологии (именно поэтому Роберт и выбрал эту компанию). В основном его работа связана с оптимизацией (например, недавно выпущенной игры Surge 2). До этого он преподавал в Дармштадтском техническом университете (Германия), самостоятельно разрабатывал небольшие видеоигры, а также работал в небольшой видеоигровой компании.
Кроме того он часто проводит лекции в университетах и выступает на конференциях.  Kha, Kinc и Krom — основные проекты Роба. Это низкоуровневые мультимедийные фреймворки для геймдева. Их можно сравнить с SDL, но с дополнительными возможностями для кросс-платформенной работы с графикой и кросс-компиляцией (как кода игровой логики, так и графических шейдеров). Kinc написан на C/C++. Kha — на Haxe и использует Kinc для работы на нативных платформах (Windows, Linux, Mac OS, Android, iOS, Nintendo Switch и т.д.), кроме того, Kha работает и в вебе (WebGL и Canvas).  А Krom — это среда исполнения JavaScript, но без поддержки Web-API. Вместо них имеются мультимедийные API, предоставляемые Kinc, а JS используется как скриптовый язык для быстрой работы с графикой, звуком и т.д. без необходимости компиляции. Krom работает на движке Chakra от Microsoft и в настоящее время используется в основном для отладки приложений на Kha.  Наиболее популярные проекты, использующие Kha — это Armory3D и ArmorPaint:  RPG Playground — еще один довольно популярный проект, использующий Kha. Его можно назвать RPG Maker’ом для веба.  Таким образом, Kha / Kinc / Krom позволяют создавать как простые браузерные 2D-игры, так и крупные 3D-проекты. Но давайте перейдем к основной теме доклада — игре CrossCode:
CrossCode — это Action RPG, получившая довольно высокие оценки. Кроме того, это довольно большая игра — прохождение основной кампании занимает около 30 часов, а выполнение дополнительных заданий может занять еще 20 часов.   CrossCode доступна на Windows, MacOS и Linux и должна в скором времени выйти на PlayStation 4, Xbox One и Nintendo Switch. Но так как CrossCode изначально создавалась как HTML5-игра, то перед Робертом встала задача портировать ее на эти консоли.
Что такое “HTML5-игра” в контексте CrossCode?  Давайте “пойдем по стеку” и зададимся вопросом “Что такое JavaScript?”: если вы ничего не знаете о нем, то можете предположить, что это скриптовый язык, имеющий что-то общее с Java :)
Что же, пойдем дальше по этой ветке (но рано или поздно мы выберемся из стека, обещаю)...  А что такое Java? Java — это объектно-ориентированный язык программирования.  Так, а что такое объектно-ориентированное программирование?
Есть два основных языка, сформировавших современные представления о том, что является объектно-ориентированным программированием — это Simula и Smalltalk.  Simula — это язык, в котором были введены понятия класса, наследования, производного класса, виртуальных процедур.  Если говорить об этих особенностях Simula, то их довольно просто реализовать (как это сделал Бьёрн Страуструп при создании C++). Например, наследование можно реализовать с помощью вложенных структур, а виртуальные процедуры — с помощью указателей на функции. Таким образом, с технической точки зрения их можно считать не более чем синтаксическим сахаром, построенным поверх процедурного языка программирования, с системой некоторых ограничений.  А вот со Smalltalk все сложнее. Да, он позаимствовал из Simula модель классов, но при этом: Данные особенности трудно реализуемы в процедурных языках программирования.  Из Smalltalk в свое время родился язык Self — также объектно-ориентированный, но в отличие от Smalltalk он основан не на классах, а на прототипах: Если хотите узнать о Self немного больше, то рекомендую посмотреть небольшой фильм, посвященный данному языку программирования.  Разобравшись немного с ООП, вернемся к Java (т.к. теперь мы сможем понять, чем является данный язык):
Как язык программирования Java можно считать подобной Simula — язык также основан на классах. C технической точки зрения Java несет в себе немного и от Smalltalk: JIT-компиляция и автоматическое управление памятью в форме сборки мусора (garbage collection). Но эти технические особенности мало влияют на Java, как на язык программирования.  И, наконец, мы вернулись к JavaScript:  И так как задача Роберта — портировать CrossCode на консоли, то поговорим немного о них:  ПК-версия CrossCode вышла в сентябре 2018 года, и издатель Deck13 Spotlight искал разработчика, который смог бы портировать игру на консоли. Этим человеком оказался Роберт. Сначала казалось, что все пойдет как по маслу — игра не выглядела слишком сложной с технической точки зрения — двухмерная графика с видом сверху. Роберт надеялся, что сможет заниматься им как второстепенным проектом. Однако, все пошло не так.  Первоначальная оценка сложности проекта:  Impact — это игровой движок для создания 2D-игр на HTML5. Его разработка началась еще в 2010 году, так что его можно считать одним из первых. Долгое время Impact был платным, и только в 2018 году стал бесплатным и доступным по лицензии MIT.  За выходные Роберту удалось портировать одну из демок с Impact на Kha, это оказалось довольно просто. На основании этого небольшого эксперимента был составлен общий план портирования CrossCode на консоли — рассматривались 3 возможных варианта развития событий (от простого к сложному):  Роберт приступил к работе по первому варианту: В итоге запустить игру на PlayStation 4 удалось довольно быстро.  Начальная секция игры работала быстро на мощном ПК, но недостаточно быстро на PlayStation. Чтобы ускорить игру Роберт заменил некоторые части, написанные на JavaScript, переписав их на C++, но этого все равно было недостаточно. Дальнейшие оптимизации давались все сложнее, т.к. основная работа выполнялась на стороне JavaScript, а для интерпретатора JavaScript не было профайлера. Поэтому попытки ускорить игру фактически осуществлялись наугад.   В итоге Роберт отказался от решения с интерпретатором и перешел ко второму варианту — использованию JIT-компилятора JavaScript:
Роберт уже был знаком с V8 — он пробовал его, когда выбирал движок для Krom, но отказался от его использования, т.к. посчитал его избыточным. По этой же причине он не стал использовать V8 и для CrossCode. Поэтому Роберт начал смотреть в сторону JavaScriptCore — это тоже довольно крупный проект, но для него есть порт, входящий в состав EA Webkit — у него открытый исходный код, он уже работает на современных консолях и используется в играх от Electronic Arts. Однако EA Webkit оставил ощущение того, что проект был открыт скорее из-за того, что в нем используется лицензия GPL, а не потому что в Electronic Arts хотели сделать его общедоступным, кроме того, даже компиляция проекта оказалась непростой задачей.  В итоге Роберт остановился на самом неочевидном варианте — Chakra — JIT-компилятор от Microsoft. По опыту Роберта работать с ним оказалось проще всего, он неплохо показывал себя в сравнительных тестах. Однако дальнейшая судьба Chakra не ясна, т.к. в Microsoft приняли решение перевести свой браузер Edge на Chromium, проект пока что продолжает поддерживаться, но неизвестно сколько это продлится. Но в контексте CrossCode Роберт посчитал, что и в текущем состоянии Chakra будет достаточно.  Роберту удалось скомпилировать Chakra для одной из консолей (для какой именно — секрет).
Затем удалось завести JIT-компилятор — он начал компилировать код, выделять для него память и загружать в нее сгенерированный машинный код.
Следующим шагом был переход к выполнению сгенерированного кода. Что произошло дальше — тайна, покрытая NDA.   Однако, чтобы вы могли примерно догадаться, Роберт привел аналогию из мира iOS. Что бы произошло, если бы вы попытались портировать свой JIT-компилятор под iOS?
Вам бы удалось: На основании этого вы можете попробовать догадаться, как это соотносится с разработкой под консоли, но никто вам не скажет точно является ли это истиной.  В итоге по какой-то “неизвестной причине” Роберту пришлось отказаться от идеи использования JIT-компилятора JavaScript. Принять это решение было непросто, т.к. уже была проделана довольно большая работа.   Поэтому пришлось перейти к работе по третьему сценарию — полностью переписать игру.
К этому моменту у Роберта уже сформировалось понимание того, что CrossCode — это очень большой проект. Именно поэтому Роберт считал данный сценарий невыполнимым: игра слишком большая, чтобы завершить проект в разумные сроки, кроме того для игры до сих пор выходят обновления, и Роберту пришлось бы также постоянно портировать и эти обновления.
И хотя у нас есть успешные примеры смены технологии, например, Minecraft, но над Minecraft работала команда. В одиночку такого не провернуть.  Роберт начал искать другой путь. И начал он с попытки создания Ahead-of-Time (AOT) компилятора для JavaScript. Проблема была только в том, что никто до этого не достиг существенных успехов в этом начинании.  С некоторыми ухищрениями Роберту удалось запустить игру в таком режиме — показывался начальный экран с логотипом и главным героем. Но затем начались буквально “темные дни”.  Позднее осознание масштабов задачи:
При запуске CrossCode выполняет большое количество работы — игра загружает ~1 GB json-файлов и подготавливает все данные. Все это происходит до того, как вы просто увидите начальный экран игры. Именно поэтому эти дни были “темными” — все, что Роберт видел в течение нескольких месяцев на своем мониторе — это черное окно игры. И в течение всего этого времени он не мог показать визуального прогресса в работе над портом.   Другая проблема — скорость работы игры. В самых “тяжелых” сценах игре требовалось ~10 мс на отрисовку кадра (и это на мощном ПК с процессором с частотой ядра 3.4 ГГц). Проблема усугубляется тем, что приложения на JavaScript не могут одновременно использовать несколько ядер (если, конечно, вы не используете WebWorker API, а в CrossCode это API не используется). Поэтому на консолях, где частоты процессоров существенно ниже, задача выдать стабильные 60 фпс, используя тот же самый код, работающий в одном потоке, выглядит мало выполнимой: вы не можете использовать JIT-компилятор, но при этом от вас требуется, чтобы ваш JavaScript-код работал быстрее, чем в V8 (а над V8 работал не один человек, и не один год). Роберт признался, что он сразу бы отказался от работы над портированием проекта, если бы знал об ожидающих его проблемах.  Масштабы игры также добавляют сложностей — всегда есть вероятность того, что код на выходе из вашего самописного AOT-компилятора может содержать ошибки, при этом ошибки могут проявляться не сразу, а спустя часы после начала игровой кампании. И так как прохождение только основного сюжета занимает порядка 30 часов, то тестирование и выявление таких потенциальных ошибок становится реально непростым занятием.  И по опыту Роберта для крупных проектов, в которых все взаимосвязано, действует закон Мёрфи. Даже если вы пишете идеальный код, всегда есть вероятность того, что в нем возникнет ошибка. И эта вероятность растет по мере роста проекта и его сложности.  Что потребуется для работы нашего AOT-компилятора и какие особенности JavaScript он должен поддерживать? Естественно нам понадобится сборщик мусора, ведь мы имеем дело с JavaScript.
Работа сборщика мусора особенно критична в случае, когда мы говорим об играх. Ведь мы хотим, чтобы код работал плавно, стабильно выдавая 60 фпс. Тогда для каждого кадра у нас есть ограниченный “бюджет” в 16,6 мс, часть из которого неизбежно будет забирать сборщик мусора — в те моменты, когда запускается сборка мусора, мы будем видеть скачки в плавности работы нашей игры. И на устройствах с относительно слабыми процессорами (например, в консолях), эти скачки будут заметны сильнее. Когда вы пишете код на языке со сборкой мусора, вы неизбежно будете генерировать мусор, так уж устроены эти языки. Кроме того, некоторые конструкции таких языков сами по себе могут генерировать мусор. Именно поэтому нам нужен быстрый и надежный сборщик мусора.  Нам также понадобится поддержка замыканий, которые сами по себе могут генерировать мусор — для того, чтобы “захватить” переменную a, потребуется выделить под нее память в динамической области памяти (и затем освободить эту память с помощью сборщика мусора).  Необходима поддержка работы с объектами в стиле JavaScript, когда к свойствам объекта можно обращаться не только с помощью оператора ., но и как к значениям в ассоциативном массиве, то есть с помощью квадратных скобок. И, как мы помним, расширение объектов в JavaScript (как и в Self) основано на прототипах: поэтому если искомое свойство не найдено в самом объекте, то его необходимо искать в его прототипе, а если оно не найдено в прототипе, то будем искать в прототипе прототипа и т.д.  Это приводит нас к вопросу типизации кода и ее влиянии на скорость работы программ. Рассмотрим данный вопрос на примере простого обращения к свойству объекта:
xx = b.x;, где x — это число. В Си это выражение будет преобразовано в 1 или 2 машинные инструкции, и в таком коде будет возможен только 1 “кэш-промах” (cache miss). В JavaScript все будет намного-намного сложнее: Естественно, что такой код может работать в сотни раз медленнее аналогичного C-кода (если только вы не знаете все типы заранее, тогда этот код можно будет оптимизировать до уровня C-кода).   Так как перед Робертом стоит только одна задача — портировать CrossCode на консоли, то создаваемый JavaScript-компилятор: И не стоит забывать про ограниченные временные рамки — фанаты ждут версию для Nintendo Switch (в сообществе CrossCode это стало мемом — каждый раз, когда разработчики публикуют какую-нибудь новость в соц.сетях, то одним из первых комментариев является “Switch when?”)!  Давайте рассмотрим существующие решения в области типизации кода для JavaScript и создания AOT-компиляторов:
Flow от Facebook может осуществлять статический анализ типов и поддерживает аннотации типов (поэтому в случае, когда не удается автоматически вывести используемый тип, можно явно указать его).  TypeScript — аналогичный инструмент, работающий с расширенной версией JavaScript.  Есть относительно недавнее интересное исследование “AOT компиляция JavaScript”, авторы которого заявляют, что им удалось создать работающий AOT-компилятор. К сожалению самого компилятора нет в открытом доступе, но принцип его работы известен (хотя он оказался недостаточно хорош для целей Роберта) — компилятор генерирует две версии кода: Авторы исследования заявляют, что полученный на выходе код работает примерно в 2 раза медленнее кода в больших JS-движках (по мнению Роберта такие заявления несут мало смысла, т.к. из них не ясны причины, почему этот код работает медленнее, будет ли код на выходе всегда медленнее и т.д.).  Роберт также нашел диссертацию Пола Биггара, посвященную разработке AOT-компилятора для PHP — данная работа может оказаться полезной, если вы решите написать свой компилятор.  Но не пытайтесь задавать вопросы в Интернете — вы можете оказаться в ситуации с бесконечной рекурсией:
На Реддите есть пост, посвященный анонсу версии CrossCode для консолей. Кто-то в комментариях к нему высказался о том, что уже существуют решения для конвертации JavaScript в C++. На вопрос Роберта о том, где можно узнать об этом поподробнее, он получил совет обратиться к разработчикам CrossCode :)  Роберт также нашел проект NectarJS — AOT-компилятор для JavaScript (и не только). По заявлению его автора сгенерированный на выходе код может работать быстрее скомпилированного кода на Си. При этом в качестве доказательства он приводит пример кода для вычисления числа Фибоначчи. Это довольно специфичный пример, т.к. в нем заранее известны все типы на входе и выходе из функций, в нем не осуществляется работа с JavaScript-объектами, поэтому этот код легко оптимизируется. Так что по мнению Роберта это пример, который на самом деле ничего не доказывает.   В итоге Роберт решил использовать Haxe.
И вот почему: Тогда все что остается — это задача по преобразованию JavaScript-кода в Haxe. Но она, к сожалению, усложняется за счет того, что:  Но давайте рассмотрим поближе код движка Impact и то, как в нем используются прототипы: в Impact объявляется объект ig.Class, который реализует собственную систему классов (как в Java :) ) — довольно распространенный прием в мире JavaScript (в современном JavaScript также появилась поддержка классов, которая делает то же самое, но уже с поддержкой на уровне самого языка).
Можно считать, что Роберту повезло, что прототипы используются именно таким образом, т.к. это означает, что достаточно парсить только вызовы к объекту ig.Class и преобразовывать их в классы Haxe. Проблему с прототипами для случая CrossCode можно считать решенной! И все благодаря тому, что JavaScript-программисты используют этот язык как Java.  Проблема с загрузкой кода также значительно упрощается, если реализовывать ее только для случаев, используемых в движке Impact:  Компилятор Роберта находит подобные вызовы, осуществляет анализ зависимостей между загружаемыми модулями игры и загружает и инициализирует их в нужном порядке (т.к. порядок загрузки модулей имеет значение — модули, зависящие от других модулей, не могут работать до тех пор, пока их зависимости не будут загружены).  Хотя в Haxe и есть замыкания, но их работа несколько отличается от замыканий в JavaScript — в JavaScript указатель на this не захватывается автоматически, для этого необходимо использовать встроенный метод bind.
Поэтому в некоторых случаях в Haxe-функциях (аналогах JavaScript-функций) пришлось добавить дополнительный параметр.  Но сходство Haxe с JavaScript обманчиво, и попытки подменить один язык другим без учета их особенностей ведут к появлению неожиданных багов. Например, Роберт попытался использовать абстрактные типы Haxe для имитации преобразований типов в JavaScript, но столкнулся с тем, что преобразования абстрактных типов работают иначе (и в результате преобразований получались не те типы). В итоге он отказался от их использования.  Еще одна проблема, связанная с отличиями Haxe и JavaScript — это работа с числами. Как уже упоминалось, в JavaScript все числа — это 64-битные числа с плавающей точкой. При этом и в Haxe и в JavaScript есть математическая библиотека, включающая в себя такие функции как Math.round/ceil/floor. Но в Haxe эти функции возвращают целые числа (что вполне логично), а целые числа в Haxe — 32-битные. И это означает, что используя их, вы теряете точность. Если не знать об этом, то можно нарваться на очень интересные проблемы. Тут Роберту опять относительно повезло, т.к. он наткнулся на эту проблему в одной из сцен в игре, когда на экране отображалось такое большое число, и в какой-то момент (когда это число стало слишком большим, чтобы поместиться в 32 бита) игра попросту зависла. Вот такой визуальный баг-репорт. Обойти эту проблему оказалось просто, т.к. в Haxe есть функции Math.fround/fceil/ffloor, возвращающие числа с плавающей точкой.  Вот еще одно отличие: и в Haxe и в JavaScript вы можете объявить литерал объекта, но их отличие в том, что если в JavaScript (начиная со стандарта ECMAScript 2015) проитерировать по всем полям такого объекта, то порядок свойств будет таким же, как и при объявлении объекта. А в Haxe порядок свойств при итерировании по ним не определен. Это отличие Роберт также обнаружил не сразу и ему пришлось поправить это поведение на стороне Haxe.  И раз уж мы упомянули неопределенное поведение, то стоит сказать, в Haxe не все строго определено. Это обусловлено тем, что Haxe может компилироваться под совершенно разные платформы, у каждой из которых свое поведение, и если пытаться поддерживать одинаковое поведение на всех платформах, то это неизбежно ударит по скорости работы кода на выходе, поэтому такая неопределенность является компромиссом между скоростью и универсальностью языка.  Учитывая размер проекта, Роберту требовалось одинаковое поведение. Поэтому порт CrossCode собирается только под hxcpp. В начале Роберт пробовал компилировать полученный Haxe-код в JavaScript, надеясь, что это ускорит разработку, но из-за различий в поведении ему пришлось отказаться от этой идеи. По мнению Роберта для данного проекта это не является существенной проблемой, но вам все же стоит знать о том, что такие отличия существуют.  В качестве альтернативы hxcpp (hxcpp — библиотека для поддержки C++ в Haxe) Роберт также рассматривал возможность использования HL/C, который конвертирует байт-код виртуальной машины HashLink в C-код. Код, полученный с помощью HL/C, компилируется быстрее, однако hxcpp-рантайм обеспечивает лучшую производительность. По этой причине Роберт выбрал hxcpp — для CrossCode скорость выполнения кода важнее скорости его компиляции.  В Deck13 для ускорения компиляции проектов используется IncrediBuild — пакет программ, позволяющих распределить выполнение “тяжелых” задач (например, компиляции C++ кода) между компьютерами, объединенными в сеть. Однако при работе из дома Роберту пришлось найти другое “решение” данной проблемы — им оказались старые point-and-click адвенчуры :) Они идеально подходят для того, чтобы скоротать время в ожидании, когда завершиться компиляция проекта: такие игры можно запустить в окне рядом с окном компилятора, их можно поставить на паузу в любое время, и от них можно легко отвлечься и перейти к отладке проекта.  Теперь давайте рассмотрим улучшения в Kha и hxcpp, появившиеся благодаря работе над CrossCode:
Во-первых, была улучшена поддержка работы со звуком в Kha. В Kha реализован свой звуковой микшер (audio mixer), и реализован он на Haxe. Благодаря этому пользователи могут легко вносить изменения в его работу. Но по этой же причине, на его работу влияет сборщик мусора: когда hxcpp запускает процесс сборки мусора, происходит остановка всех потоков, выполняется сборка мусора, и затем выполнение потоков продолжается. При работе со звуком такие кратковременные паузы особенно критичны — при воспроизведении звуковых эффектов могут возникать “заикания”. Но теперь, благодаря помощи со стороны Хью Сандерсона (автора hxcpp), код звукового микшера в Kha может работать независимо от сборщика мусора.  Во-вторых, удалось найти и исправить несколько ошибок в hxcpp, связанных с поддержкой Юникод.  Полноценная поддержка Юникод появилась в Haxe 4, и до недавнего времени она была отключена в hxcpp по-умолчанию. Протестировать поддержку Юникод в hxcpp помогло сообщество пользователей Kha — Роберт включил ее в форке hxcpp, распространяемом вместе с Kha.  В-третьих, удалось исправить ошибки в работе сборщика мусора, основанном на поколениях (generational garbage collector). По-умолчанию в hxcpp использовался не такой быстрый алгоритм сборки мусора, но при этом его реализация была хорошо оттестирована. Включив в hxcpp новый сборщик мусора, Роберту удалось избавиться от “подвисаний” игры, связанных с работой сборщика мусора, но спустя примерно 30 секунд игра “упала”.
С помощью Хью Сандерсона удалось исправить ошибки в работе сборщика мусора: кое-что Роберт смог поправить самостоятельно, другие проблемы удалось изолировать и тогда Роберт обращался к Хью с примерами, в которых найденные проблемы повторялись.
Кроме того, Роберт снова воспользовался помощью сообщества Kha, включив generational garbage collector по-умолчанию и предупредив пользователей о возможных проблемах. К счастью, к этому моменту все критические проблемы уже были исправлены — сборщик мусора теперь работал стабильно, и сон у Роберта стал крепче :)  Следующую часть доклада Роберт посвятил тому, как его компилятор выводит типы переменных:
Изначально типы всех переменных неизвестны — им присваивается тип Unknown. Затем компилятор может определить типы данных:  Остальные типы:  Рассмотрим подробнее выведение типов в компиляторе Роберта:
Компилятор проходит по всему коду проекта, и когда один тип “встречает” другой, компилятору нужно выполнить кучу разных вещей.
Самый простой вариант дальнейших действий компилятора — это “выравнивание типов”. Пример такого сценария — обращение к полю объекта. Встретив в коде выражение типа a.x, компилятор сначала определит тип объекта a (экземпляром какого класса является объект a), таким образом компилятор сможет определить тип поля x данного объекта. В идеале тип поля x и тип присваиваемого ему значения должны совпадать. Если же выведенные типы не совпадают, то компилятору необходимо “выровнять” их, сделав их совместимыми. Для этого тип поля x должен быть “повышен” до совместимого типа. Например, Boolean может быть повышен до Double и даже до Object, т.к. для JavaScript такие преобразования валидны. Но в то же время Double не может быть повышен до Object — для такого случая тип поля будет заменен на Multiple.   Более сложный сценарий выведения типов в компиляторе — коллизии типов (type clash), которые могут происходить: В таких ситуациях переменная a должна иметь возможность хранить значения того типа, который уже был ей присвоен, плюс значения типа переменной b. Тип переменной b при этом обычно не изменяется, но тип переменной a обычно “повышается”.  Также компилятор обрабатывает сценарий “мягких” коллизий типов (soft type clash), которые могу возникать при сравнении двух переменных — в JavaScript у вас есть возможность сравнить переменную, хранящую численное значение, и null или undefined. В таких случаях выведенный тип переменной повышается до Multiple.  Процесс выведения типов переменных разделен на три фазы, каждая из которых происходит не за один проход, а в цикле — до тех пор, пока компилятор не найдет новых типов.
Основная цель каждой из фаз — свести к минимуму число переменных с типом Undecidable, т.к. они сильнее всего влияют на скорость работы программы.
На начальной фазе выполняется выведение типов без возможности присвоения Undecidable-типа.
На второй фазе типизация выполняется уже с возможностью присвоения Undecidable-типа, но только для переменных, все еще имеющих Unknown-тип (то есть для тех переменных, которым еще не был присвоен другой тип). Обычно на этом этапе большинство переменных с типом Undecidable становятся переменными с другими типами.
Далее, для того, чтобы избавиться от оставшихся в коде Undecidable-переменных, Роберт вносит точечные изменения в код конвертера. Таким образом, например, Роберту удалось избавиться от Undecidable-переменных, получаемых в результате парсинга JSON.  Компилятор также выполняет “унификацию типов”. Для чего она нужна?
В JavaScript-коде часто используются литералы объектов, но они не попадают в систему классов, построенную ранее, и это негативно сказывается на скорости работы игры (т.к. если не пытаться типизировать данные объекты, то компилятор будет использовать для них табличный тип).
Поэтому компилятор Роберта ищет в коде все определения литералов объектов и пытается конвертировать их в классы (в случаях, когда это имеет смысл). Для этого компилятор анализирует как объявленные объекты используются: какие поля они имеют при объявлении, переменным каких типов они присваиваются. На основании данного анализа компилятор создает новый класс, который и используется в качестве типа.
Это была сложная задача, но в итоге ее удалось решить.  Что пока не удалось решить, так это ограничения в компиляторе, связанные с Mutliple-типом: в коде часто встречаются места, где одна переменная может хранить значения разных типов, и было бы гораздо эффективнее “запоминать” эти типы, но на данный момент компилятор просто преобразует такую переменную к Mutliple-типу.
И что еще хуже, компилятор можно “обмануть”:   Для отлавливания подобных ошибок Роберт добавил специальный режим компиляции, при котором выполнение программы сопровождается проверками типов переменных (при каждом присваивании) и аргументов функций (при каждом вызове функций). Игра при этом работает очень и очень медленно, но при возникновении подобных ошибок можно сразу увидеть, где ошибка возникла и какие при этом типы использовались (и тогда такую ошибку можно исправить).  Конечно же, в CrossCode используется eval — метод, с помощью которого можно выполнить JavaScript код, представленный строкой. Поэтому Роберту пришлось реализовать интерпретатор JavaScript, который может взаимодействовать с откомпилированным JavaScript-кодом. К счастью потребовалось реализовать поддержку только небольшого подмножества JavaScript, использовавшегося в вызовах eval.  Используя описанные решения, Роберту удалось довести порт до играбельного состояния, игру можно было пройти, но она работала недостаточно быстро. Основная причина относительно низкой скорости работы игры заключалась в том, что компилятор часто выбирал Multiple-тип для переменных. Чтобы избавиться от избыточного использования Multiple, Роберт стал “помогать” компилятору с помощью “подсказок типов” (type hints), добавляя в JavaScript-код комментарии с именами типов, которые компилятор должен использовать для переменных и аргументов функций. И это оказалось одним из основных методов улучшения скорости работы CrossCode — анализируя места в коде, где часто используется Multiple, а также причины, почему так происходит, Роберт добавляет комментарии с подсказками, а остальную работу выполняет его компилятор.  Используя подсказки типов всего в нескольких местах, Роберту удалось значительно ускорить работу CrossCode — игра заработала быстрее, чем в V8 (на текущий момент игра работает в несколько раз быстрее, чем в V8).  Пока что дату релиза порта Роберт назвать не может, но говорит, что релиз будет очень скоро:   Подводя предварительные итоги, можно сказать что Haxe: Но, в конце концов, Роберт считает, что Haxe все-таки больше помог, нежели помешал ему (но насколько точно — сказать сложно).  Используя накопленный опыт портирования CrossCode, Роберт в свободное время начал экспериментировать с написанием “правильного” компилятора JavaScript, но уже без использования Haxe (но ожидать релиза универсального компилятора, по словам Роберта, все же не стоит, если только этим проектом не заинтересуются инвесторы).  Если вы захотите написать свой компилятор JavaScript, то теперь вы примерно знаете, что от вас потребуется:  Роберт считает, что раз ему удалось в одиночку создать свой неуниверсальный компилятор JavaScript для CrossCode (а он не считает себя специалистом по компиляторам), то создание универсального компилятора JavaScript — вполне реализуемая задача, особенно если ей займется команда опытных программистов.  Роберт также считает, что чем больше JavaScript-программисты “деградируют” в Java-программистов (то есть используют JavaScript как Java, насколько это возможно), то тем проще достичь хорошей производительности JavaScript-кода и скомпилированного из него кода. Именно поэтому ему удалось написать свой AOT-компилятор.  И немного о Deck13 Spotlight — издателе CrossCode:
Если вы не хотите писать свой компилятор JavaScript (или заниматься подобными вещами), то этим может заняться Роберт, как сотрудник компании :)  Deck13 Spotlight занимается издательством небольших инди-игр и весомым доводом в пользу сотрудничества с ними является возможность привлечения опытных разработчиков из студии Deck13. Поэтому издательство сможет помочь инди-компаниям по всем вопросам, связанным с разработкой и портированием игр. Спасибо за внимание! Программист