В эти чудесные январские дни всех нас, конечно, волнует вопрос минимизации исходного кода с сохранением инварианта. В смысле, не волнует?!? Зря… Вот упал у вас компилятор, а программа гигантская — как-то неудобно такое разработчикам слать. И тут начинается веселье: а если вот это выкинуть? О, не падает — ладно, оставляем, а если это? — всё ещё падает, и это, и это, и то… Ой, я компилятор на старых исходниках запускал. В то же время, автоматизация поиска багов — дело-то житейское: git bisect, llvm-bugpoint, creduce,… В статье я опишу yet another способ решения проблемы упрощения тестового случая, более-менее универсальный по отношению к языку программирования, и покажу некоторые примеры использования. Универсальное, говорите… Может, оно конечно, уже десять раз реализовано, но разве это остановит матёрого велосипедиста. Да и когда в руках микроскоп, все проблемы кажутся гвоздями. В роли микроскопа — PMD. В общем, есть такой язык для написания моделей, описываемых дифференциальными уравнениями — Modelica. Он довольно продвинутый, имеется весьма крутая открытая реализация и вообще. Но есть маленькая проблема: иногда в компиляторе возникает Internal compiler error. В прошлой статье я уже показывал, как добавить поддержку Моделики в статический анализатор PMD (кстати, в процессе review всплыли некоторые мои ошибки) в дополнение к десятку уже имеющихся языковых модулей. Вот и сейчас я решил — чего добру пропадать — и прислал proof-of-concept инструмента Source Code Minimizer, переиспользующего имеющиеся языковые модули PMD. К сожалению, меня послали — правда, не далеко, в соседний репозиторий: ментейнер сказал, что поддерживать это до скончания века он пока не решается, да и затеряется оно в общей кодовой базе, поэтому вскоре я обнаружил у себя в почтовом ящике приглашение в collaborator'ы свежесозданного pmd/pmd-scm. Во-первых, какова постановка задачи? Требуется уменьшить размер исходного кода, сохраняя некоторое его свойство. Конкретнее, хочется, чтобы В этом разделе я примерно опишу реализацию. Для начала ещё раз замечу, что идея эта, мягко говоря, не нова. И если git bisect я привёл просто в качестве примера «механизма автоматической отладки», то вот на creduce или что-то похожее я натыкался уже довольно давно (правда, не пользовался). А вот llvm-bugpoint использовать приходилось — впечатляет: сидишь, отлаживаешь свой LLVM Pass, а он — зараза такая — падает на некоторых сишных файлах. Так вот, можно этот сишный файл скомпилировать в LLVM bitcode, запустить opt на .bc-файле со своим плагином и убедиться, что он падает. А потом, грубо говоря, я просто заменил opt на llvm-bugpoint, и через минуту получил здоровенный «скелет» одной из функций, состоящий из тучи базовых блоков и переходов между ними; всё, кроме ветвлений, было успешно выкинуто. Кстати, как и в моей постановке задачи, после десятка минут ручного упрощения всё свелось к тому, что я некорректно обрабатывал один из видов ветвлений, а всё остальное — просто декорации. В общем, идея не нова. А теперь о реализации. Поскольку предполагалось более-менее универсальное средство, хотелось сделать некий фреймворк, в который бы можно было бы запихнуть реализации, оптимизированные под разные языки. В итоге выделилось две достаточно ортогональных концепции: Один из вариантов свойства, которое может хотеться сохранить, я уже описал: «компилятор напечатал в консоль некую фразу» («Internal compiler error», например). Также идеи можно почерпнуть из разнообразных фаззеров: AFL, Killerbeez и прочих: Часть из них я уже реализовал (код возврата, напечатанная строка), часть — нет, какие-то, возможно, будут специфичны для отдельных языков. В общем, это достаточно автономный кусок задачи, часто вообще не зависящий от конкретного языка. На первый взгляд, здесь всё сугубо языкозависимое. Где-то нужно выкидывать переменные вместе со случаями использования, где-то — поддерживать одинаковое количество уравнений и неизвестных. Тем важнее абстрагировать эту часть кода. Но что-то базовое можно сделать общим для всех: например, лёгким движением руки движок XPath-правил превращается… превращается… — ой, для версии XPath 1.0 не превращается. Извините, маленькая техническая неувязочка — в универсальное средство обрезки синтаксических деревьев на зиму. Вообще, API стратегии минимизации довольно простое: по большому счёту, у стратегии вызывается функция шага (ей передаётся список корневых узлов синтаксических деревьев), которая может через интерфейс операций попросить «а попробуй вот эти ветви выкинуть». Если инвариант нарушился, функция возвращает управление, если нет — раскручивает стек выкидыванием специального исключения, а полученный вариант принимается в качестве следующего приближения. Процесс считается завершённым, если функция шага вернула управление не через исключение. Вы, возможно, скажете, что это жутко не эффективно — может быть и так, ЗАТО УДОБНО!!111, но о чём вообще речь, если предполагается штатно в цикле сотни раз запускать внешний процесс компилятора. Тут возникает вопрос: как из прореженного AST обратно получить исходник? Конечно, можно для каждого языка попробовать написать специальный код, генерирующий текстовый файл. Но, как известно, программист должен быть ленивым. Так что не пойдёт — это будет уже явно не из серии «подхватили существующие реализации и вперёд». К счастью, в узлах дерева имеется информация о начальных и конечных строке и столбце для этого узла — значит, если языковой модуль корректно указывает эту информацию, можно взять исходный текст и аккуратно вырезать из него куски (отсюда, кстати, и некоторая сложность с обфускацией: для этого недостаточно что-то выкидывать, а нужно заменять: идентификаторы, например). Кстати, в кодовой базе PMD даже обнаружился класс для редактирования файла посредством не пересекающихся операций вырезания текста (а также добавления, но конкретно для этой задачи это не так интересно). На данный момент у меня реализовано две стратегии. Одна из них — это обрезка по XPath, являющаяся в каком-то смысле вырожденным случаем, поскольку принудительно записывает результат, даже если он уже не является синтаксически корректным исходником. Вторая уже «честная» итерационная и интерактивная (в том смысле, что она реально взаимодействует с компилятором и проверяет инвариант): она просто пробует выкидывать веточки по порядку по одной в цикле. Вот о ней — чуть подробнее: Для начала, соберём минимизатор: Теперь нужен какой-нибудь исходник. Давайте, например, возьмём GreedyStrategy.java. При помощи Rule Designer выясним, как выглядит типичное AST для Java, и запустим То есть, мы опустошили все функции, непосредственно содержащие больше одного statement. Впрочем удаление комментариев, пустых строк и т.д. у меня не специфицировано — всё-таки это (пока?) в первую очередь инструмент для отладки компиляторов, а не создания красивых описаний интерфейсов. Давайте теперь рассмотрим что-нибудь поинтереснее: попробуем минимизировать одновременно два файла с обратной связью от компилятора: orig/TestResource.java: orig/Main.java: Как вы видите, они не компилируются: Давайте представим, что с первой ошибкой что-то не то, и попробуем сделать минимальный пример, выдающий Для этого запустим В итоге получаем TestResource.java: Main.java: Всё, как заказывали! Пока что проект находится на достаточно ранней стадии, но уже есть, что продемонстрировать. В дальнейшем есть идеи сделать API для language agnostic указания зависимостей между узлами AST, сделать возможность предоставлять стратегии, специфичные для языка. Ещё было бы неплохо сделать универсальную стратегию, выполняющую скрипт на Groovy/Kotlin/ещё чём-нибудь — всё-таки пользоваться этим будут люди, которые и Java-то, может, никогда не видели, зато в совершенстве знают, например, Моделику, и имеют в голове свои продвинутые способы ужимания исходников. Программист