пятница, 28 января 2011 г.

Подсчет числа установленных битов в 9-битовом поле

Задача подсчета установленных битов возникла при разработке библиотеки для решения японской игры судоку. В судоку нужно заполнять клетки цифрами от 1 до 9 так, чтобы ни в одной горизонтальной и вертикальной линиях (поле в игре представлено линиями 9x9 и разбито на 9 квадратов 3x3), а также ни в одном из квадратов, не было одинаковых цифр. С точки зрения алгоритма решения задачи, состояние каждой клетки удобно хранить в 9-битовом поле, где установленными битами помечены "выбывшие" цифры; соответственно поле без установленных битов означает, что в клетку можно вставить любую из 9 цифр, а если, скажем, в поле остался не установленным единственный 6-ой бит, то в эту клетку можно поместить только цифру 6.
Алгоритм подсчета установленных битов в 32-битном слове прекрасно описан в книге Генри Уоррена мл. "Hacker's Delight" (которую по непонятным причинами перевели на русский как "Алгоритмические трюки для программистов"). Алгоритм по своей сути является рекурсивным и основан на методе дихотомии: сначала 32-битное слово делится пополам и задача разбивается на 2 одинаковых подзадачи, в свою очередь, получившиеся 16-битные половинки также делятся пополам, добавляя еще 4 подзадачи, и так происходит до тех пор, пока всё слово не будет разделено на 16 2-битовых полей, в каждом из которых биты подсчитываются 16-ю независимыми подзадачами. Сам автор приводит пример, изображенный на рисунке ниже. Поскольку алгоритм подсчета битов рекурсивен, то его удобней изображать снизу вверх: соответственно нижние 16 подзадач изображены в верхней части рисунка сразу после исходного значения в слове, а итоговый результат находится внизу:
На языке C этот алгоритм можно представить следующим образом:
    x = ( x & 0x55555555 ) + ( ( x >>  1 ) & 0x55555555 );
    x = ( x & 0x33333333 ) + ( ( x >>  2 ) & 0x33333333 );
    x = ( x & 0x0F0F0F0F ) + ( ( x >>  4 ) & 0x0F0F0F0F );
    x = ( x & 0x00FF00FF ) + ( ( x >>  8 ) & 0x00FF00FF );
    x = ( x & 0x0000FFFF ) + ( ( x >> 16 ) & 0x0000FFFF );
В верхнем выражении используется маска 0x55555555, которая в двоичном выражении соответствует циклическому повторению 01; первое слагаемое выделяет биты в этой маске (т.е. все нечетные биты), во втором слагаемом сначала все биты сдвигаются вправо на одну позицию и к полученному значению применяется та же маска (таким образом, выделяются все четные биты). Два полученных значения складываются и результат подобным же образом обрабатывается маской 0x33333333, которая соответствует циклическому повторению 0011. И так далее. Легко видеть, что маски соответствуют "стенкам" битовых полей, характерных для текущего этапа алгоритма дихотомии; "стенки" обозначены на рисунке красными разделителями. Маска подбирается таким образом, чтобы каждое отделение между "стенками" было заполнено ровно наполовину единицами справа и нулями слева.
Наша задача - адаптировать данный алгоритм для 9-битового поля. Для хранения нашего битового поля выберем целочисленный тип с достаточной емкостью (например 8-битный char не подходит), пусть это будет 32-битный int. Пусть наше битовое поле находится в 9 младших битах. Принудительно занулять старшие биты нет необходимости, так как на работу алгоритма они не повлияют.
К сожалению, 9 не делится на 2, поэтому нужно выбрать другое число: 3 - отличный (и единственный) кандидат. 3 в степени 2 и есть 9, соответственно нам понадобятся всего два шага (заметьте, что 2 в пятой степени равно 32, поэтому предыдущий алгоритм выполнялся за 5 шагов). С другой стороны, раз мы поменяли базу с 2 на 3, то вместо двух слагаемых на каждом шаге нам понадобятся 3, что соответствует двум сдвигам битов для выделения по маскам.
Давайте приведем функцию, которая выполняет подсчет:
  int  countSetBits9( int  nmb )
  {
      nmb =  ( nmb & 0x49 ) + ( ( nmb >> 1 ) & 0x49 ) + ( ( nmb >> 2 ) & 0x49 );
      return ( nmb & 0x07 ) + ( ( nmb >> 3 ) & 0x07 ) + ( ( nmb >> 6 ) & 0x07 );
  }
Маска 0x49 соответствуе двоичному числу 001001001, а 0x07 - это 000000111. Маски соответствуют заполнению единицами справа на треть отделений битового поля, разделенных "стенками", характерными для двух этапов алгоритма.

среда, 26 января 2011 г.

Простейший пример шаблонного метапрограммирования в C++

Под метапрограммированием обычно понимают предоставление специальных средств внутри или снаружи исходного кода, которые позволяют выводить часть программы автоматически. Таким спецсредством в C++ являются шаблоны (templates). Шаблоны C++ обеспечивают особый метаязык, по своим характеристикам являющийся функциональным и понимаемый исключительно компилятором C++. С функциональными языками шаблонный метаязык сближают наличие общих подходов (сопоставление с образцом, рекурсия) и запрет переменных-состояний.
Рассмотрим следующую ситуацию. Допустим мы хотим создать класс, услуги которого базируются на сходных по назначению, но отличных в их реализации классах BaseA и BaseB. Если оба базовых класса обладают схожим интерфейсом, то логично оформить наш класс в виде шаблона:
    template  < typename  Base >
    class  Derived  : public Base
    {
    };
(Я не буду заострять внимание на содержимом классов, поэтому наш класс пустой, хотя фактически он может содержать множество полей и методов.)
Подразумевается, что вместо шаблонного аргумента Base нужно будет подставлять BaseA либо BaseB в зависимости от требований разработчика. А теперь главный вопрос: что если разработчику необходима информация о базовых классах объектов, основанных на шаблоне Derived. Как ее получить?
На вскидку я могу перечислить четыре способа:
  1. Вполне возможно, что классы BaseA и BaseB сами предоставляют информацию о своих типах. Но это не всегда так, а изменять эти классы мы не можем.
  2. Использование RTTI и dynamic_cast. Во-первых, это не всегда доступно, ведь для использования dynamic_cast необходимо, чтобы BaseA и BaseB были полиморфными, т.е. содержали виртуальные функции. Во-вторых, использование RTTI чревато потерей эффективности.
Оставшиеся два способа связаны с введением в шаблон класса Derived нового метода GetBaseClass(), предоставляющего информацию о базовом типе:
    template  < typename  Base >
    class  Derived : public Base
    {
        public:
            BaseClass  GetBaseClass( void ) const;
    };
BaseClass - это enum, перечисляющий базовые классы, а также значение Prohibited:
    enum  BaseClass
    {
        Prohibited,
        BaseClassA,
        BaseClassB
    };
Итак, оставшиеся способы:
  1. Специализация шаблона Derived для отдельных типов базового класса. К сожалению, шаблон класса Derived может содержать большое число полей и методов, его полная специализация приведет к нежелательному дублированию кода.
  2. Использование трюка, основанного на шаблонном метапрограммировании.
Четвертый способ - это и есть то, что я хотел здесь показать. Он основан на введении вспомогательного шаблона класса (или структуры) BaseClassInstance, не содержащего ничего, кроме статической константы типа BaseClass, и специализированного для разных фактических типов базовых классов:
    template  < typename >
    struct  BaseClassInstance
    {
        static const BaseClass  value = Prohibited;
    };


    template  <>
    struct  BaseClassInstance< BaseA >
    {
        static const BaseClass  value = BaseClassA;
    };


    template  <>
    struct  BaseClassInstance< BaseB >
    {
        static const BaseClass  value = BaseClassB;
    };
(В первом определении после ключевого слова typename можно было указать имя аргумента, например Base, но, поскольку оно не используется в теле шаблона, я его опустил.)
Несмотря на то, что по форме здесь перечисляются разные специализации шаблона структуры C++, по сути, если рассматривать это с точки зрения функционального программирования, здесь объявлены клозы простейшей функции, возвращающей значение типа BaseClass в зависимости от результата сопоставления с образцом (типом - шаблонным аргументом)!
Нам осталось написать тело метода GetBaseClass():
    template  < typename  Base >
    BaseClass Derived< Base >::GetBaseClass( void ) const
    {
        return BaseClassInstance< Base >::value;
    }
Теперь объекты классов, основанных на шаблоне Derived, обладают информацией о своем базовом классе. Эта информация закладывается в момент инстанцирования классов, а значит нет дополнительных нагрузок в ран-тайме, и, что не менее важно, нам не пришлось дублировать исходный код.

пятница, 31 декабря 2010 г.

Чиним f-spot

Я использую f-spot в качестве менеджера коллекций фотографий на своем домашнем компьютере. К сожалению, начиная с версии 0.8.0 f-spot испортился: тамбнейлы превратились в серые квадратики, а при клике на них фотографии открывались на долю секунды и f-spot тут же падал. Поиск в интернете вывел на баг #627745. В двух словах баг заключается в том, что если в пути к файлам с фотографиями встречаются директории с нелатинскими символами, то f-spot благополучно падает. В пути к моим фото присутствует директория Снимки/, так что мне не повезло.
Некоторое время я терпеливо ждал выхода следующей версии f-spot. Но, f-spot 0.8.2 вышел, а проблема осталась. Пришла пора действовать. Итак, для восстановления работоспособности f-spot была применена стратегия, состоящая из двух основных пунктов:
  1. Сделать так, чтобы путь к файлам с фотографиями не содержал нелатинских символов
  2. Сделать так, чтобы f-spot узнал об изменениях в пункте 1
Первый пункт достигается очень просто: нужно использовать старые добрые символические ссылки:
ln -s Снимки Pictures
Теперь к фотографиям можно обращаться через путь Pictures/ вместо Снимки/.
Чтобы реализовать второй пункт, нужно знать как f-spot хранит информацию о фотографиях. А делает он это с помощью базы данных photos.db, которая обычно находится в директории ~/.config/f-spot/. Редактировать эту БД можно с помощью утилиты sqlite3. Естественно, перед тем как изменять БД, неплохо создать ее резервную копию, просто скопировав файл photos.db. Итак, открываем БД для внесения нужных изменений:
sqlite3 photos.db
Эта команда загружает интерактивную среду sqlite. Для просмотра перечня таблиц вводим
sqlite> .tables
exports         meta            photo_versions  rolls         
jobs            photo_tags      photos          tags
Поля отдельной таблицы можно увидеть с помощью команды .schema:
sqlite> .schema photos
CREATE TABLE photos (
    id          INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 
    time            INTEGER NOT NULL, 
    base_uri        STRING NOT NULL, 
    filename        STRING NOT NULL, 
    description     TEXT NOT NULL, 
    roll_id         INTEGER NOT NULL, 
    default_version_id  INTEGER NOT NULL, 
    rating          INTEGER NULL 
);
Увидеть, где находятся данные, содержащие пути с директорией Снимки/ было просто: достаточно выполнить select * from для всех (или только подозрительных) таблиц. Оказалось, что данные о путях содержатся в полях base_uri таблиц photos и photo_versions.
Теперь нужно выполнить замену Снимки на Pictures во всех данных для этих полей:
sqlite> update photos set base_uri=replace(base_uri,"Снимки","Pictures")
   ...> where base_uri like "%Снимки%";
sqlite> update photo_versions set base_uri=replace(base_uri,"Снимки","Pictures")
   ...> where base_uri like "%Снимки%";
Всё. Выходим из среды sqlite:
sqlite> .quit
Теперь запускаем f-spot: тамбнейлы должны появиться, при клике на них фотографии должны показываться без проблем. Осталось изменить имя директории, в которую будут импортироваться фото в дальнейшем c Снимки на Pictures: это можно сделать прямо в f-spot из меню Правка -> Параметры.

P.S. Похоже, что этот баг не связан напрямую с f-spot, возможно он присутствует на уровне mono или Gnome.

P.P.S. Я пробовал использовать shotwell, но f-spot мне по-прежнему кажется симпатичней, несмотря на свою глючность. Возможно, в дальнейшем я перейду на shotwell, когда его допилят.

воскресенье, 19 декабря 2010 г.

Вышел Geant4 9.4 и ChargeExchangeMC (aka Cexmc)

17 декабря вышла новая версия библиотеки для моделирования прохождения элементарных частиц в веществе Geant4 9.4. Geant4 широко применяется при моделировании физических экспериментов, а также в медицине и космофизике.
Для меня данный релиз замечателен тем, что в него в качестве одного из advanced examples вошла программа ChargeExchangeMC, автором которой я являюсь. Программа применялась для моделирования реального эксперимента в Петербургском Институте Ядерной Физики. Первая версия, написанная на фортране, была в дальнейшем переписана с нуля на C++ с использованием Geant4.
Основные особенности программы:
  1. Это достаточно большой проект, он включает 71 заголовочный файл и 63 файла исходников, общий размер программного кода превышает 20 тысяч строк
  2. Геометрия установки полностью описана в формате GDML. Это позволит гибко изменять описание установки без изменения исходного кода
  3. Для упрощения анализа данных используются гистограммы на основе библиотеки CERN ROOT
  4. Реализована модель реконструкции данных с гибкой настройкой различных параметров
  5. В программе реализован гибкий алгоритм на основе шаблонов C++ для возможности легкого изменения изучаемых взаимодействий
  6. Персистентное хранение данных реализовано с помощью библиотеки boost::serialization
  7. Пользовательский фильтр данных позволяет изменять полученные данные с помощью специальных скриптов. Грамматика скриптов описана с помощью библиотеки boost::spirit
  8. Run manager имеет уникальный режим проигрывания событий (replay mode), который позволяет пересчитывать уже имеющиеся данные с параметрами, отличными от исходных
Документацию по установке и использованию программы можно найти здесь.

четверг, 9 декабря 2010 г.

Реорганизация структуры репозитория Subversion

Ситуация: Имеется репозиторий Subversion, который управляет среднего размера проектом, скажем my_project, уже в течение года. Соответственно, присутствует богатая история изменений. Но... Изначально структура репозитория была спланирована недальновидно, в частности были допущены следующие две оплошности:
  1. Импорт исходного кода осуществлялся в корневую директорию, выделенную для Subversion
  2. Не была создана промежуточная директория trunk/. Напомним, что в Subversion реализация хранения тегов и веток (в том числе главной) не различаются: фактически это просто разные поддиректории внутри проекта. Поэтому в Subversion обычно предлагается структурировать проект с использованием следующих канонических поддиректорий: trunk/ (для главной ветки), branches/ (для побочных веток) и tags/ (для тегов)
Первый пункт означает, что невозможно нормальным способом добавить еще один проект в корневую директорию Subversion (у меня она расположена в ~/svn/). Второй пункт означает, что невозможно нормальным способом создать теги и ветки для существующего проекта. Именно со второй проблемой я и столкнулся, когда всплыла необходимость выделить отдельную ветку для поддержания проекта при использовании старой версии одной из сторонних библиотек.

Постановка задачи: изменить существующий (или создать новый) репозиторий Subversion, в котором будет возможно создавать новые проекты, и в котором структура существующего проекта будет приведена к канонической (с использованием трех поддиректорий trunk/, branches/ и tags/), и при этом будет сохранена в работоспособном состоянии история всех изменений данного проекта без добавления служебных ревизий в ее начало.

Решение: будем создавать новый репозиторий. Для этого сначала выполним дамп существующего репозитория в файл:
svnadmin dump ~/svn/ > my_project_svn_dump
Теперь, предварительно забекапив директорию ~/svn/, удаляем все, что в ней находится, и создаем новый репозиторий для проекта my_project:
svnadmin create ~/svn/my_project
Теперь нам нужно загрузить дамп истории в новый репозиторий. Но предварительно его следует отредактировать (дамп репозитория представляет собой обычный ASCII файл), сохранив на всякий случай исходную копию. Поскольку в старый репозиторий проект был импортирован из директории my_project/, а имя нового репозитория уже содержит название my_project, то нам нужно удалить все упоминания об этой директории внутри файла. Однако, мы поступим проще: нам ведь все равно нужна поддиректория trunk/, так что зачем удалять упоминания о my_project/ внутри дампа, когда это можно просто заменить на trunk/? Итак, ищем строку
Node-path: my_project
и заменяем ее на
Node-path: trunk
Кроме того, my_project входит в состав путей к файлам, поэтому ищем все вхождения
Node-path: my_project/
и заменяем их на
Node-path: trunk/
Замену вхождений легко автоматизировать с помощью текстового редактора, например vim.
А теперь загружаем отредактированный дамп старого репозитория в новый:

svnadmin load ~/svn/my_project < my_project_svn_dump
Если все прошло успешно, то нас можно поздравить: новый репозиторий будет содержать всю историю изменений из старого репозитория без каких-либо дополнений.
Осталось добавить директории branches/ и tags/ (я перенесу части строки для удобства, на самом деле это однострочная команда):
svn mkdir -m "branches and tags directories added"
    file:///home/user/svn/my_project/tags
    file:///home/user/svn/my_project/branches
Теперь можно делать чекаут из нового репозитория и продолжать работу с проектом:
svn co file:///home/user/svn/my_poject/trunk my_project
Disclaimer: Обязательно делайте бекап репозитория перед какими-либо изменениями внутри него. Также неплохо сохранять копию дампа репозитория до тех пор, пока вы не осознали до конца, что все изменения прошли успешно.

суббота, 4 декабря 2010 г.

Отключение автоматического монтирования отдельных разделов диска устройства, подключаемого через USB

Имеется в наличии медиаплеер Iconbit HDR12L, в который установлен внутренний диск размером 500Гб. Диск был размечен устройством следующим образом:
Устр-во Загр     Начало       Конец       Блоки     Id  Система
/dev/sdc1           16065   967386104   483685020    7  HPFS/NTFS
/dev/sdc2       967386105   967707404      160650   82  Linux своп / Solaris
/dev/sdc3       967707405   976125464     4209030   83  Linux
/dev/sdc4       976125465   976446764      160650   83  Linux 
При обмене файлов с компьютером интерес представляет только первый раздел с NTFS системой. Но по умолчанию в моем дистрибутиве (Fedora 14) монтируются все три раздела (исключение составляет 2-ой раздел с подкачкой). Соответственно, задача формулируется следующим образом: отключить автоматическое монтирование разделов /dev/sdc3 и /dev/sdc4 при подключении медиаплеера через USB.
За работу со съемными носителями в Linux традиционно отвечают две подсистемы: udev и hal. Первая подсистема привязывает устройства к корневой файловой системе в директории /dev, а вторая работает на более высоком уровне и определяет что делать с новым устройством: для съемных носителей это что обычно заключается в монтировании носителя в корневую файловую систему. Соответственно, нас интересует уровень hal и мы создаем новое правило hal для медиаплеера iconbit и помещаем его в новый файл /etc/hal/fdi/policy/90-iconbit.fdi. Вот его содержимое:
<?xml version="1.0" encoding="UTF-8"?>

<deviceinfo version="0.2">
  <device>
    <match key="volume.label" string_outof="iconbit_p3;iconbit_p4">
      <merge key="volume.ignore" type="bool">true</merge>
    </match>
  </device>
</deviceinfo>
На этом примере видно, что правила hal записываются в формате xml. В нашем правиле будут проверяться метки разделов: если метка раздела совпадает с iconbit_p3 или iconbit_p4, то он не будет монтироваться (значение volume.ignore для таких разделов устанавливается в true). Важно отметить, что удобные метки разделов я установил сам. Вместо проверки на совпадение метки раздела можно установить проверку на совпадение с uuid раздела: в этом случае ключом будет не volume.label, а volume.uuid. Атрибут string_outof соответствует выбору между несколькими вариантами, разделенными точкой с запятой; если нужно осуществлять проверку только для одного варианта, то вместо string_outof нужно использовать string.
Всё. Прекрасно. Перезапускаем hal и оно ... не работает. Проблема в том, что в новых дистрибутивах (в Fedora начиная с версии 11) для работы со съемными носителями вместо hal используется подсистема udisks из DeviceKit. В udisks несколько иной подход, и здесь нам таки придется добавлять правила на уровне udev. Итак, создаем файл /etc/udev/rules.d/90-iconbit.rules co следующим содержимым:
# iconbit auxiliary partitions
ENV{ID_FS_TYPE}=="ext3", \
  ENV{ID_FS_LABEL}=="iconbit_p3|iconbit_p4", \
  ENV{UDISKS_PRESENTATION_HIDE}="1"
Теперь, после перезагрузки компьютера, цель достигнута: при подключении медиаплеера к компьютеру через USB монтируется только один раздел с NTFS.
Примеры использования правил udisks можно найти в файле  /lib/udev/rules.d/80-udisks.rules.

Update. В Fedora 17, а может быть даже раньше, система udisks была заменена на udisks2, в которой синтаксис правил опять изменился. Теперь правило для iconbit выглядит так:
# iconbit auxiliary partitions
ENV{ID_FS_TYPE}=="ext3", \
  ENV{ID_FS_LABEL}=="iconbit_p3|iconbit_p4", \
  ENV{UDISKS_IGNORE}="1"
Кроме того, файлы правил теперь находятся в директории /usr/lib/udev/rules.d/. И еще, лично я переименовал 90-iconbit.rules в 98-iconbit.rule (хотя конкретное значение численного префикса в названии файла не играет существенной роли до тех пор, пока указанные в нем правила не пересекаются с правилами из других файлов в этой директории).

пятница, 5 ноября 2010 г.

Реализация парсера пользовательского языка на Haskell

Итак, я выкладываю версию парсера пользовательского языка с поддержкой арифметических выражений с функциями и переменными, написанную на функциональном языке Haskell. С грамматикой языка можно ознакомиться, прочитав мое первое сообщение в этом блоге. Задачей программы, как и прежде, будет построение абстрактного синтаксического дерева (AST) для отдельных инструкций языка. В отличие от версии, написанной на boost::spirit, я не стал реализовывать базовый эвалюатор полученных деревьев, хотя сделать это не сложно.

В качестве лексера и грамматического анализатора я использовал alex и happy соответственно, которые входят в состав Haskell Platform; alex анализирует исходный код лексера (с расширением .x), а happy анализирует исходный код грамматики (с расширением .y). Обе программы генерируют соответствующие файлы с расширением .hs. Фактически, исходные коды лексера и грамматики состоят по большей части из кода на Haskell (все, что находится в этих файлах в фигурных скобках, является валидным кодом Haskell), и лишь небольшое количество мета-объявлений предназначено для предварительной обработки alex и happy.

Описание грамматики языка для happy совместимо с описанием для boost::spirit несмотря на то, что внутренне эти два генератора парсеров реализованы по-разному (happy генерирует LALR парсер, а spirit - парсер с рекурсивным спуском). Хотя проблема левой рекурсии в happy исчезает, и, кроме того, happy предоставляет дополнительные возможности вроде указания приоритета операторов, я не стал изменять реализацию грамматики по сравнению с версией для boost::spirit.

Итак, грамматика нашего пользовательского языка в версии haskell / happy выглядит следующим образом:

Statement : Action Condition { ParseResult $1 $2 }

Action : keep tpt    { KeepTpt }
       | keep edt    { KeepEdt }
       | delete tpt  { DeleteTpt }
       | delete edt  { DeleteEdt }

Condition :: { Subtree }
Condition : if Expression { buildTopTree $2 }

Expression :: { Node }
Expression : OrExpression { $1 }

PrimaryExpression : Function1 { buildFunction1Subtree $1 }
                  | '(' Expression ')' { setPriorityInOperatorTree 0 $2 }
                  | LeafOperand { Leaf $1 }

LeafOperand : Constant { Constant $1 }
            | Variable { Variable $1 }

Constant : double { DoubleValue $1 }
         | integer { IntegerValue $1 }

Variable : identifier '[' integer ',' integer ']' { Vector2 $1 $3 $5 }
         | identifier '[' integer ']' { Vector1 $1 $3 }
         | identifier { Scalar $1 }

Function1 : identifier '(' Expression ')' { Function1 $1 $3 }

OrExpression : AndExpression '|' OrExpression
               { buildOperatorSubtree ( Op ( LogOp Or ) 1 False ) $1 $3 }
             | AndExpression { $1 }

AndExpression : Relation '&' AndExpression
                { buildOperatorSubtree ( Op ( LogOp And ) 2 False ) $1 $3 }
              | Relation { $1 }

Relation : Addition RelOperator Addition
           { buildOperatorSubtree ( Op ( RelOp $2 ) 3 False ) $1 $3 }
         | Addition { $1 }

Addition : Multiplication AddOperator Addition
           { buildOperatorSubtree ( Op ( AddOp $2 ) 4 False ) $1 $3 }
         | Multiplication { $1 }

Multiplication : UnaryExpression MultOperator Multiplication
                 { buildOperatorSubtree ( Op ( MultOp $2 ) 5 False ) $1 $3 }
               | UnaryExpression { $1 }

UnaryExpression : UnaryOperator PrimaryExpression
                  { buildUnaryOperatorSubtree ( Op ( UnaryOp $1 ) 6 True ) $2 }
                | PrimaryExpression { $1 }

UnaryOperator : '-' { UMinus }
              | '!' { Not }

MultOperator : '*' { Mult }
             | '/' { Div }

AddOperator : '+' { Plus }
            | '-' { Minus }

RelOperator : "<=" { LessEqual }
            | ">=" { MoreEqual }
            | "!=" { NotEqual }
            | "<" { Less }
            | ">" { More }
            | "=" { Equal }

happy пользуется определениями токенов из лексера, импортируя его модуль CfLexer. Эти токены (keep, delete, tpt, edt, if, арифметические операторы) сопоставляются с типами лексера и перечислены в секции %token. В фигурных скобках описания грамматики указываются семантические действия, которые должны соответствовать построению объекта необходимого типа, как это сделано и в boost::spirit. Поскольку в Haskell реализован автоматический вывод типов на основе модели Хиндли-Милнера, то формально указывать типы выражений нет необходимости, хотя в двух случаях, для наглядности, я определил типы выражений Condition :: { Subtree } и Expression :: { Node }. Типы выражений определены ниже в этом же файле:


data  Statement =
    ParseResult
    {
        action    :: Action,
        condition :: Subtree
    }

data  Action =
    KeepTpt                  |
    KeepEdt                  |
    DeleteTpt                |
    DeleteEdt
    deriving Show

data  Node =
    Tree Subtree             |
    Leaf LeafOperand
    deriving Show

data  Subtree =
    Subtree
    {
        nodeType :: NodeType,
        children :: [ Node ]
    }
    deriving Show

data  NodeType =
    Operator  Operator       |
    Function  Function
    deriving Show

data  Operator =
    Op
    {
        op         :: OperatorType,
        priority   :: Int,
        hasRLAssoc :: Bool
    }

data  OperatorType =
    Uninitialized            |
    Top                      |
    UnaryOp  UnaryOperator   |
    MultOp   MultOperator    |
    AddOp    AddOperator     |
    RelOp    RelOperator     |
    LogOp    LogOperator

data UnaryOperator =
    UMinus                   |
    Not

data MultOperator =
    Mult                     |
    Div

data AddOperator =
    Plus |
    Minus

data RelOperator =
    LessEqual                |
    MoreEqual                |
    NotEqual                 |
    Less                     |
    More                     |
    Equal

data LogOperator =
    And |
    Or

data  Function =
    FunctionName  String

data  LeafOperand =
    Variable  Variable       |
    Constant  Constant
    deriving Show

data  Constant =
    DoubleValue   Double     |
    IntegerValue  Int

data  Variable =
    Scalar   String          |
    Vector1  String Int      |
    Vector2  String Int Int

data Function1 =
    Function1
    {
        fName :: String,
        fExpr :: Node
    }

Все типы являются алгебраическими в понятии Haskell, то есть создаются с использованием ключевого слова data и могут иметь один и более конструкторов. Некоторые из типов наследуют определения класса Show, что позволит выводить их на экран компьютера. Поскольку дефолтный вывод некоторых типов на экран меня не удовлетворил, я переопределил для них функцию show:


instance Show Operator where
    show = showOperator

showOperator ( Op a _ _ ) = 
    "-op- " ++ case a of
                    Uninitialized   -> "UNINITIALIZED"
                    Top             -> "Top"
                    UnaryOp UMinus  -> "u -"
                    UnaryOp Not     -> "!"
                    MultOp Mult     -> "*"
                    MultOp Div      -> "/"
                    AddOp Plus      -> "+"
                    AddOp Minus     -> "-"
                    RelOp LessEqual -> "<="
                    RelOp MoreEqual -> ">="
                    RelOp NotEqual  -> "!="
                    RelOp Less      -> "<"
                    RelOp More      -> ">"
                    RelOp Equal     -> "="
                    LogOp And       -> "&"
                    LogOp Or        -> "|"

instance Show Function where
    show = showFunction

showFunction ( FunctionName a ) = "-fun- " ++ a

instance Show Variable where
    show = showVariable

showVariable ( Scalar a ) = a
showVariable ( Vector1 a b ) = a ++ "[" ++ show b ++ "]"
showVariable ( Vector2 a b c ) = a ++ "[" ++ show b ++ "," ++ show c ++ "]"

instance Show Constant where
    show = showConstant

showConstant ( DoubleValue a ) = show a
showConstant ( IntegerValue a ) = show a

Окончательный вывод на экран дерева для отдельной инструкции будет осуществляться с помощью функции printResult и ее вспомогательными функциями:


printResult :: Statement -> IO ()
printResult x = do
    putStrLn $ "Result: action = " ++ show ( action x ) ++ ", parsed tree = "
    putStrLn ""
    printSubtree ( condition x ) ""

printSubtree :: Subtree -> String -> IO ()
printSubtree ( Subtree a b ) is = do
    putStrLn $ is ++ case a of
                        Operator aa -> show aa
                        Function aa -> show aa
    printChildren b $ is ++ "    "

printChildren :: [ Node ] -> String -> IO ()
printChildren [] _ = return ()

printChildren ( Tree x : xs ) is = do
    printSubtree x is
    printChildren xs is

printChildren ( Leaf x : xs ) is = do
    putStrLn $ is ++ printLeaf x
    printChildren xs is

printLeaf :: LeafOperand -> String
printLeaf a = case a of
                    Variable aa -> show aa
                    Constant aa -> show aa

При построении AST использовались вспомогательные функции:


buildTopTree :: Node -> Subtree
buildTopTree ( Tree x ) = x
buildTopTree x@( Leaf _ ) =
    Subtree ( Operator $ Op Top 0 False ) [ x ]

buildFunction1Subtree :: Function1 -> Node
buildFunction1Subtree x =
    Tree $ Subtree ( Function . FunctionName $ fName x ) [ fExpr x ]

buildOperatorSubtree :: Operator -> Node -> Node -> Node
buildOperatorSubtree a@( Op _ _ True ) nl nr =
    Tree $ Subtree ( Operator a ) [ nl, nr ]

buildOperatorSubtree a nl nr@( Tree sr@( Subtree ( Operator b ) ( cnrl : _ ) ) )
    | p == priority b =
        Tree $ moveDownLeft ( Subtree ( Operator a ) $
                                      nl : [ getDeepestLeft cnrl p ] ) sr
    | otherwise =
        Tree $ Subtree ( Operator a ) [ nl, nr ]
        where p = priority a

buildOperatorSubtree a nl nr =
    Tree $ Subtree ( Operator a ) [ nl, nr ]

getDeepestLeft :: Node -> Int -> Node
getDeepestLeft x@( Leaf _ ) _ = x

getDeepestLeft x@( Tree ( Subtree ( Operator b ) ( cnl : _ ) ) ) p
    | p == priority b = getDeepestLeft cnl p
    | otherwise = x

getDeepestLeft x _ = x

moveDownLeft :: Subtree -> Subtree -> Subtree
moveDownLeft sl ( Subtree b@( Operator _ ) ( ( Leaf _ ) : cnrr ) ) =
    Subtree b $ Tree sl : cnrr

moveDownLeft sl@( Subtree ( Operator _ ) _ )
             ( Subtree b@( Operator _ )
                       ( ( Tree ( Subtree ( Function _ ) _ ) ) : cnrr ) ) =
    Subtree b $ Tree sl : cnrr

moveDownLeft sl@( Subtree ( Operator a ) _ )
             ( Subtree b@( Operator _ )
                       ( ( Tree csrl@( Subtree ( Operator c ) _ ) ) : cnrr ) )
    | priority a == priority c =
        Subtree b $ Tree ( moveDownLeft sl csrl ) : cnrr
    | otherwise =
        Subtree b $ Tree sl : cnrr

moveDownLeft sl _ = sl

buildUnaryOperatorSubtree :: Operator -> Node -> Node
buildUnaryOperatorSubtree a x = 
    Tree $ Subtree ( Operator a ) [ x ]

setPriorityInOperatorTree :: Int -> Node -> Node
setPriorityInOperatorTree i ( Tree ( Subtree ( Operator ( Op a _ x ) ) y ) ) =
    Tree $ Subtree ( Operator $ Op a i x ) y

setPriorityInOperatorTree _ x = x

Функция buildTopTree проверяет, является ли ее аргумент, представленный типом Node, листом, если это так, то она возвращает простое поддерево (Subtree) с корнем - оператором типа Top и единственным листом - переданным ей аргументом, если же аргумент - дерево, то buildTopTree возвращает его представление типа Subtree. Функция buildOperatorSubtree  и вспомогательные функции getDeepestLeft и moveDownLeft строят поддерево для бинарных операторов и сдвигают лево-ассоциативные операторы (т.е. все бинарные операторы) вправо (т.е. вглубь дерева) относительно операторов с таким же приоритетом: это позволяет исправить правоассоциативные правила грамматики для левоассоциативных операторов. Функция setPriorityInOperatorTree позволяет избежать порчи дерева при сдвиге левоассоциативных операторов для выражений, содержащихся в скобках. Функции buildFunction1Subtree и buildUnaryOperatorSubtree довольно простые и делают то, что от них требует их название.

Это практически все. Осталось упомянуть, что тестовые примеры я поместил в файл cf.hs, а компиляция программы основана на использовании Makefile, поэтому достаточно только распаковать пример и запустить команду make.

Кроме того, хотелось бы сказать несколько слов о сравнительной производительности программ, написанных с использованием boost::spirit и haskell / happy. Я промолчу о скорости компиляции: итак всем понятно, насколько долго будет компилироваться код на C++, использующий шаблонные объявления из boost::spirit. Меня намного больше удивила разница в скорости выполнения откомпилированных программ. Для корректности сравнения я убрал эвалюацию синтаксических деревьев из кода на boost::spirit. Так вот, код на haskell / happy выполнялся на моем компьютере (двухъядерный AMD 64) в среднем порядка 0.04 секунды, код на boost::spirit с включенной оптимизацией -O2: порядка 0.35 сек. (в 10 раз дольше!), а без оптимизации - вообще 3.4 сек., т.е. примерно в 100 раз дольше!

Исходный код примера можно взять здесь.