Как стать автором
Обновить

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

Время на прочтение5 мин
Количество просмотров24K
Haxe — очень удобный и практичный язык, но маленькое сообщество и, как результат, небольшое количество библиотек заставляют меня немало времени тратить на подготовку «заголовочных файлов» для интеграции open source библиотек в haxe. Немного об этом языке и о путях преобразования исходного кода на разных языках мне бы и хотелось рассказать ниже.

С языком программирования haxe (тогда ещё его звали haXe) я познакомился около трёх лет назад и с тех пор мы не расстаёмся. Т.к. этот язык мало освещён на Хабре, то для начала — о haxe «in a nutshell», как поётся в известной песне.

О haxe в двух словах


Для тех, кто незнаком с haxe, чуть-чуть вводной информации:
  • синтаксис этого языка почти полностью повторяет ActionScript, который в свою очередь похож на JavaScript, но с типами данных;
  • жёсткая типизация, но с автоматическим выводом типов (для простых случаев);
  • отсутствие жёстко привязанной среды выполнения — компилятор лишь транслирует haxe-код в другие языки (сейчас поддерживаются: neko, php, javascript, flash/actionscript, c++, java, c#; на подходе также python);
  • очень быстрый компилятор.

Как и другие языки, haxe не является серебряной пулей и, как мне кажется, есть две основные области, где он полезен:
  • написание мультиплатформенных приложений (здесь стоит упомянуть библиотеку для разработки игр OpenFL);
  • написание сложных js-приложений (т.к. их написание сразу на js проблемно ввиду отсутствия типизации).

Конвертируем код


Пути для преобразования исходного кода на одном языке в код на другом языке я вижу следующие:
  1. через построение полноценного дерева разбора (Abstract Source Tree = AST);
  2. через использование инструментов, умеющих преобразовывать исходные коды во что-то более простое (наподобие xml);
  3. «грубой силой» через использование регулярных выражений.

Без сомнения, математически верный путь — первый, т.к. позволяет сделать всё аккуратно и, в идеале, получить на выходе сразу компилируемый текст на другом языке. Минусы — полноценный разбор сложен, чувствителен к деталям. Почитать про построение 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<int>"
/(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<Regex>;
	
	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).
Теги:
Хабы:
+30
Комментарии24

Публикации