Haxe: конвертируем исходный код

cb0198fb8c874476b3f46e96c9299943.pngHaxe — очень удобный и практичный язык, но маленькое сообщество и, как результат, небольшое количество библиотек заставляют меня немало времени тратить на подготовку «заголовочных файлов» для интеграции open source библиотек в haxe. Немного об этом языке и о путях преобразования исходного кода на разных языках мне бы и хотелось рассказать ниже.С языком программирования haxe (тогда ещё его звали haXe) я познакомился около трёх лет назад и с тех пор мы не расстаёмся. Т.к. этот язык мало освещён на Хабре, то для начала — о haxe «in a nutshell», как поётся в известной песне. Для тех, кто незнаком с haxe, чуть-чуть вводной информации: синтаксис этого языка почти полностью повторяет ActionScript, который в свою очередь похож на JavaScript, но с типами данных; жёсткая типизация, но с автоматическим выводом типов (для простых случаев); отсутствие жёстко привязанной среды выполнения — компилятор лишь транслирует haxe-код в другие языки (сейчас поддерживаются: neko, php, javascript, flash/actionscript, c++, java, c#; на подходе также python); очень быстрый компилятор. Как и другие языки, haxe не является серебряной пулей и, как мне кажется, есть две основные области, где он полезен: написание мультиплатформенных приложений (здесь стоит упомянуть библиотеку для разработки игр OpenFL); написание сложных js-приложений (т.к. их написание сразу на js проблемно ввиду отсутствия типизации). Пути для преобразования исходного кода на одном языке в код на другом языке я вижу следующие: через построение полноценного дерева разбора (Abstract Source Tree = AST); через использование инструментов, умеющих преобразовывать исходные коды во что-то более простое (наподобие xml); «грубой силой» через использование регулярных выражений. Без сомнения, математически верный путь — первый, т.к. позволяет сделать всё аккуратно и, в идеале, получить на выходе сразу компилируемый текст на другом языке. Минусы — полноценный разбор сложен, чувствителен к деталям. Почитать про построение AST-деревьев можно в литературе по компиляторам (см., например, Ахо А., Сети Р., Ульман Дж., Лам М. — Компиляторы. Принципы, технологии, инструменты).Второй путь возможен только при наличии подходящих утилит для исходного языка. Автору доводилось использовать yuidoc при написании генератора haxe-обёртки для популярной js-библиотеки easeljs, благо последняя хорошо документирована.Третий путь — через обработку регулярными выражениями — относительно прост, хотя и требует «доводки напильником» результирующего кода. Именно об этом варианте пойдёт речь ниже.Окей, регулярка, конвертируй! Регулярные выражения имеют огромный, на мой взгляд плюс — быстро пишутся и, всего лишь, пару минусов: в принципе не могут разобрать вложенные (рекуррентные) структуры (с произвольным уровнем вложенности); тяжело читаются (а для больших выражений — и не менее тяжело пишутся). Первый недостаток, как показала практика, для большинства языков не очень критичен, особенно если нам не нужно полноценное преобразование, а нужно лишь «выдёргивание» заголовков классов и методов. Второй же можно частично обойти введя константы, хранящие небольшие кусочки регулярных выражений и позволив использовать их для написания более сложных конструкций.В результате приходим к наборам правил преобразования, где есть два вида этих самых правил: объявления констант и регулярные выражения для поиска/замены. Вот фрагмент файла правил для преобразования из c# в haxe:

ID = \b[_a-zA-Z][_a-zA-Z0–9]*\b LONGID = ID (?:[.]ID)* TYPE = LONGID (?:[<]\s*LONGID(?:\s*,\s*LONGID)*\s*[>])?

// «int[]» => «Array» /(TYPE)\s*\[\s*\]/Array<$1>/

// «int v» => «var v: int» /(TYPE)\s+(ID)/var $2:$1/ Дело остаётся за малым — написать инструмент, который бы принимал на вход файлы с исходными текстами и файл regex-правил, а на выходе выдавал бы файлы с результатом применения этих правил. И такая утилита была написана (refactor). Ниже я приведу немного кода, чтобы показать (я надеюсь) простоту и лаконичность языка haxe.

Рассмотрим код класса, читающего файл с правилами, разбирающего — где константы, а где регулярки, и строящего массив регулярных выражений для последующего преобразования исходных файлов:

import stdlib.Regex; // используем класс Regex из библиотеки stdlib, т.к. стандартный EReg недостаточно умный в смысле замены import sys.io.File;

// using ниже подмешивает static-методы класса StringTools ко всем строкам; // например, у класса String нет метода replace (); // мы могли бы писать StringTools.replace («abc», «b», «z»), но благодаря using можем писать «abc».replace («b», «z»); // однако, всё это лишь сахар — при компиляции сгенерируется код с обычным вызовом static метода using StringTools;

class Rules { // здесь (default, null) говорит о том, что мы объявляем переменную, // которую позволительно читать извне (default), а вот менять — нельзя (null); // бывает ещё «never» — когда нельзя делать операцию не только извне, но и внутри класса public var regexs (default, null) : Array; public function new (rulesFile: String) { var text = File.getContent (rulesFile); // тип для text будет выведен автоматически regexs = []; var lines = text.replace (»\r»,»).split (»\n»); // тип данных для lines не указываем, компилятор выведет сам var consts = new Array<{ name:String, value:String }>(); // также тип данных легко выводится автоматически

// for в haxe только такой — в формате foreach; // перебрать числа от 0 до 9 можно так: for (n in 0…10) for (line in lines) { line = line.trim (); if (line == » || line.startsWith (»//»)) continue; var reConst = ~/^([_a-zA-Z][_a-zA-Z0–9]*)\s*[=]\s*(.+?)$/; // регулярка для детектирования константы if (reConst.match (line)) { var value = reConst.matched (2); for (const in consts) { value = replaceWord (value, const.name, const.value); } consts.push ({ name: reConst.matched (1), value: value }); } else { for (const in consts) { line = replaceWord (line, const.name, const.value); } regexs.push (new Regex (line.replace (»\t»,»))); } } } // метод меняет константу на её значение static function replaceWord (src: String, search: String, replacement: String) : String { var re = new EReg (»(^|[^_a-zA-Z0–9])» + search + »($|[^_a-zA-Z0–9])», «g»); // map () ищет в строке src по регулярному выражению // и меняет найденное на результат выполнения функции, переданной ему вторым параметром return re.map (src, function (re) { return re.matched (1) + replacement + re.matched (2); }); } }

Автор уже три года использует haxe для написания web-приложений. Это здорово: возможность писать код клиента и сервера на одном языке + строгая типизация + синтаксис, близкий к js — всё это очень радует.Созданный инструмент refactor упростил интеграцию haxe-кода со сторонними библиотеками. Например, недавно с его помощью была создана обёртка для js-библиотеки threejs.

Надеюсь, мне удалось вас заинтересовать если не языком haxe, то хотя бы подходом к обработке исходных текстов программ. Ведь при помощи этого простого метода можно не только конвертировать программы с языка на язык, но и просто делать текст программы красивым (beautify).

© Habrahabr.ru