воскресенье, 28 марта 2010 г.

[prog] Об условиях перегенерации кода из внешних DSL-ей

Некоторое время назад я обсуждал, если не ошибаюсь, с Дмитрием Вьюковым, условия перегенерации исходного кода, который строится из внешнего DSL. Например, есть у вас описание грамматики для yacc/bison – из этого описания вы получаете исходный код на C/C++. При каких условиях полученный исходный код должен быть перегенерирован?

Очевидно – при изменении описания, из которого код строится. Именно такой подход я заложил в свой инструмент RuCodeGen. Там для DSL-ного файла подсчитывается md5-хэш, который хранится в отдельном файле, рядом с DSL-ем. Если DSL-ный файл меняется, то md5-хэш так же меняется, и RuCodeGen понимает, что нужно выполнить перегенерацию.

Но здесь есть одно “но” – перегенерация не происходит, если меняется сам кодогенератор. Грубо говоря, появляется новая версия RuCodeGen, которая генерирует новый код по тому же самому DSL. Значит, генерацию нужно провести заново. Но ведь исходный DSL не изменился. Значит, перегенерация не произойдет.

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

Дима Вьюков озвучивал другой способ: по DSL-описанию код строится всегда. Но фиксируется в результирующих файлах только в случаях, когда новый и ранее построенный код не совпадают. Т.е. изменился DSL – получился другой код – записали новый результат генерации. Или изменился генератор – все равно получился другой код – записали новый результат генерации.

Казалось бы, все классно. Я уже хотел даже заняться разработкой RuCodeGen 0.4, в которой использовался бы именно такой подход… Но наткнулся на интересный недостаток этого способа.

Дело в том, что создаваемый генератором код в моем случае нуждался в отладке. Я вставлял в него отладочные печати и даже менял в нем одни вызовы на другие чтобы посмотреть, что получится. И я мог этот делать зная, что RuCodeGen не перегенерирует ранее созданный код, поскольку DSL не менялся. А вот если бы генератор просто сравнивал новый и старый код (как предлагал Дима), то мои правки сгенерированного исходного текста просто выбрасывались бы.

Такие дела. Так что, похоже, самый лучший способ – это учитывать хэш от исходного DSL и хэш от самого кодогенератора. Если что-то из этого поменялось, значит нужно выполнять перегенерацию исходного кода.

12 комментариев:

Dmitry Vyukov комментирует...

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

Dmitry Vyukov комментирует...

Хотя честно говоря мне не очень нравится идея правки сгенерированного кода... ну по-крайней мере в идеальном мире. Тогда вся идея теряет смысл. Становится не понятно, то ли это сгенерированный код, который можно в любой момент перегенерить без каких-либо последствий; то ли это рукописный код, который никогда перетирать нельзя.

Dmitry Vyukov комментирует...

Можно сделать что-то типа хуков, как в аспектно-ориентированном программировании.
Допустим, генератор генерирует такой код:
template void do_XXX_prehook(X, Y) {}

void do_XXX_autogenerated(int x, int y)
{
do_XXX_prehook(x, y);
// AUTO-GENERATED BODY
}

Перед включением этого кода пользователь может определить:
void do_XXX_prehook(int x, int y)
{
printf("x=%d, y=%d\n", x, y);
}

И тогда управление будет попадать к нему без модификации сгенерированного кода.

Аналогичные хуки можно сделать и на post и на around (что-бы можно было обернуть сгенерированную функцию в try-catch).

eao197 комментирует...

>А где этот хэш запоминать?

У меня так -- если dsl лежит в файле pdu_scheme.rb, то рядом создается файл .pdu_scheme.rb-rucodegen.md5, в котором и лежит хэш исходного dsl.

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

>мне не очень нравится идея правки сгенерированного кода

Мне тоже. Для отлаженного кодогенератора это, в принципе, и не должно быть нужно.

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

Dmitry Vyukov комментирует...

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

Это если его не правили руками. В таком случае и мой подход будет работать.
Ты же говоришь, что сгенерированный код могут подправить руками, тогда хэш оригинального сгенерированного кода ты уже ниоткуда не получишь.
Я имел в виду, что в .pdu_scheme.rb-rucodegen.md5 ты кладешь не хэш от DSL и версии генератора, а хэш от сгенерированного кода. Когда в следующий раз ты перегенерируешь код, ты сравниваешь эти хэши и, если они различаются, - переписываешь файл.
Ориентироваться на сгенерированный код мне кажется более надёжным и удобным. Представь, ты подправил генератор и забыл продвинуть его версию. Или ты внёс в генератор изменение, которое влияет на очень маленькое кол-во кода (изменил какую-то редко используемую фичу), или добавил комментариев в DSL файл, или отступы подправил. С твоим подходом ты получишь либо лишние переписывания файла, либо наоборот не перепишешь, когда надо. Если же ориентироваться на содержимое файла - то тут железно; что-то поменялось не важно по какой причине - переписываем; ничего существенного не поменялось - не переписываем.

eao197 комментирует...

>Я имел в виду, что в .pdu_scheme.rb-rucodegen.md5 ты кладешь не хэш от DSL и версии генератора, а хэш от сгенерированного кода.

Теперь я понял о чем ты говорил. Похоже, что это хорошее решение.

ShaggyOwl комментирует...

Что приходит в голову сходу:
1. Изменить имя файла с хешем .pdu_scheme.rb-rucodegen.md5 ->
.pdu_scheme.rb-rucodegen.version.md5
Плюсы, минусы: Просто, негибко, быстро, будет стабильно работать.

2. В шапку сгенерированного файла вставлять информацию о версии кодогенератора. Заодно, туда же можно писать опции с которыми строился файлик и любую другую информацию.
Плюсы, минусы: резкое усложнение кода: надо научиться парсить как комментарий, так и код комментария; очень гибкое решение.

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

Сам бы выбрал первый вариант.

eao197 комментирует...

>1. Изменить имя файла с хешем .pdu_scheme.rb-rucodegen.md5 ->
.pdu_scheme.rb-rucodegen.version.md5
Плюсы, минусы: Просто, негибко, быстро, будет стабильно работать.


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

Более надежно было бы вместо версии использовать констрольную сумму исходников генератора, имхо.

Unknown комментирует...

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

eao197 комментирует...

>Если есть автоинкремент билдов, то менять номера не придется.

Номера билдов -- это хорошее решение для компилируемых языков. Для Ruby/Python/Perl/Tcl не сработает. А как раз на них многие генераторы легко пишутся.

ShaggyOwl комментирует...

Я спросони автоинкрементом назвал номер коммита в SVN :) (еще как и где его хранить не подумал :) )
Да еще и перелогиниться забыл.

После осмысления, вариант с хешем кажется самым простым.

eao197 комментирует...

>Я спросони автоинкрементом назвал номер коммита в SVN :) (еще как и где его хранить не подумал :) )

Когда-то потребовалось вносить в exe-шник дату и время последнего изменения исходников (это не номер билда, но очень похоже), так я поступал так:

сделал заголовочный файл, скажем, с именем mod_date_time.hpp, который не хранился под контролем версий;
при компиляции сначала запускался какой-то скрипт, который вычислял дату и время последнего изменения исходников и генерировал mod_date_time.hpp.

При этом, правда, main.cpp всегда перекомпилировался (т.е. в нем подключался mod_date_time.hpp), но это было не страшно. С номером ревизии, думаю, можно поступать так же.