Boost::Spirit: Грамматики, функции и замыкания
В этой заметке я продолжу рассказывать о библиотеке Boost::Spirit, предназначенной для написания на C++ различных парсеров. В прошлой заметке я описал базовые возможности Spirit, a в этой хочу затронуть несколько возможностей, которые по моему мнению практически необходимы в случае написания сколько-нибудь сложных парсеров. Итак, эти возможности:Группировка правил в грамматики
Как я уже писал, порядок разбора в Spirit определяется с помощью правил (rules), которые представляют из себя нетерминалы грамматики. Правила могут ссылаться друг на друга, и все вроде бы хорошо, пока грамматика не начинает расти. В этот момент хотелось бы объекдинить все правила в какую-то более крупную сущность, которой удобней манипулировать. Такой сущностью в Spirit является грамматика (grammar). Грамматика - всего-навсего набор правил, выполняющих вместе некоторый общий разбор.
Как описывается грамматика? Смотрим пример:
class MyGrammar : public boost::spirit::grammar<MyGrammar> {
public:
template<typename ScannerT> class definition {
public:
definition( const MyGrammar & self ) {
wordGroup = '[' >> *( word | wordGroup ) >> ']';
word = lexeme_d[ +chset_p("a-zA-Z") ];
}
boost::spirit::rule<ScannerT> const & start() const {
return wordGroup;
}
private:
boost::spirit::rule<ScannerT> wordGroup, word;
};
};
Грамматика описывается в виде класса, унаследованного от boost::spirit::grammar, параметризованного грамматикой же. При этом, класс грамматики внутренний класс definition, в конструкторе которого описываются правила разбора. Метод start класса definition возвращает начальное правило. При этом, стоит заметить, что определение грамматики параметризуется типом сканнера, что позволяет использовать все грамматику для различных сканнеров не меняя ее.
После того, как грамматика определена таким образом она используется как обычный парсер или правило:
MyGrammar mygram;
boost::spirit::parse( "[some [string]]", mygram, space_p );
boost::spirit::parse( "[yes],[another [string]]", mygram >> *( ',' >> mygram ), space_p );
Спрашивается, а зачем же все это тогда нужно? Предположим, что грамматика не просто осуществляет разбор, но и сохраняет в какие-то внутренние структуры результаты. В этом случае, объединение в одном классе и правил и структур с результатами оказывается очень удобным.
Более подробно о грамматиках можно прочитать в соответствующей главе руководства Spirit.
Использование лямбда-выражений в семантических действиях
В прошлый раз я рассматривал использование в семантических действиях функций и функторов. А очень хотелось бы писать там непосредственно C++ код. И Spirit предоставляет такую возможность за счет испоользвания библиотеки Phoenix. Все, что необходимо сделать, это включить файл boost/spirit/attribute.hpp и использовать пространство имен phoenix. После этого, можно использовать в правилах следующие конструкции вида:
std::string s;
word1 = lexeme_d[ +chset_p("a-zA-Z") ][ cout << arg1 ];
word2 = lexeme_d[ +chset_p("a-zA-Z") ][ var(s) = construct_<std::string>( arg1, arg2 ) ];
int n;
num1 = uint_p[ var(n) = var(n) + arg1 ];
В этой записи arg1 и arg2 - это результаты, возвращаемые парсером. Для большинства парсеров это указатели на начало и конец разобранного участка текста. Но, например, для числовых парсеров это некоторое число, для парсеров, предназначенных для считывания одного символа (например, ch_p, chset_p и их комбинации ) arg1 - это считанный символ.
Определение функций
В таких выражениях возникает необходимость вызывать какие-то функции, помимо стандартных операторов. Хочется написать что-нибудь вроде:
int someFunc( int i ) { return ...; }
int n;
num1 = uint_p[ var(n) = someFunc( arg1 ) ];
Однако, такой код работать не будет. И даже не будет компилироваться. Проблема в том, что arg1 в момент компиляции программы это вовсе не число, а объект, обозначающий место, куда это число будет подставленно в момент разбора.
Как же решить проблему? Для этого Phoenix предоставляет возможность определения функций, которые вычисляются в момент разбора. Для того, чтобы объявить такую функцию необходимо:
- Описать реализацию в виде специального класса
- Объявить объект, представляющий эту функцию
struct someFuncImpl {
template <typename ArgType>
struct result {
typedef int type;
};
int operator()(int arg) const {
return ...;
}
};
function<someFuncImpl> someFunc;
int n;
num1 = uint_p[ var(n) = someFunc( arg1 ) ];
Этот код уже является корректным. При разборе вызывается оператор () для объекта-функции и в качестве параметра в него передается значение arg1. Встает вопрос - что это за шаблон result в определении реализации? Этот шаблон - метафункция, которая вычисляет тип возвращаемого значения по типам аргументов. Ведь в принципе, Вы можете перегрзуить оператор () для различных типов, а Phoenix'у надо знать какой-именно тип имеет возвращаемое значение. Например, так:
struct someFuncImpl {
template <typename ArgType>
struct result {
typedef ArgType type;
};
int operator()(int arg) const {
return ...;
}
std::string operator()(std::string arg) const {
return ...;
}
};
В этом случае, функция может обрабатывать как числа так и строки, и шаблон result указывает, что функция возвращает тип аргумента.
Вот собственно и практически все о функциях. Небольшие моменты, которые могут оказаться полезными:
- Функции могут принимать более одного аргумента, необходимо только в описании result указать столько параметров шаблона, сколько аргументов функция принимает.
- Функции могут быть шаблонными. Точнее, шаблонным делается их оператор (). Опять же, result должен корректно описывать возвращаемое значение.
Замыкания
Последнее о чем я хочу сказать в этой заметке (но вовсе не последнее по важности) - это замыкания. По сути, замыкания - это некоторый контекст, содержащий пользовательские данные, связанные с правилами. Примеры, когда это бывает необходимо:- Хочется, чтобы правило имело возвращаемое значение не пару указателей на строку, а что-нибудь более осмысленное. Например, правило в калькуляторе может возвращать число - результат.
- Правило оперирует какими-то данными в семантических действиях. Конечно, можно определить переменную и работать с ней, а что если правило вызывает само себя? Хотелось бы, чтобы эти два вызова не влияли на данные друг друга напрямую.
- Оба перечисленных выше пункта)
struct calc_closure : boost::spirit::closure<calc_closure, double> {
member1 val;
};
rule<ScannerT, calc_closure::context_t> factor = ureal_p[factor.val = arg1];
В данном случае calc_closure - определяемое замыкание. Оно представляет из себя структуру, наследуемую от boost::spirit::closure, с первым параметром - самой структурой замыкания и остальными - типами элементов, представляющих его данные. После этого, элементы в замыкании объявляются через типы member1, member2, member<N>.
Для привязки замыкания к правилу используется параметризация типа rule еще типом <имя замыкания>::context_t.
Важной особенностью замыканий является то, что первый элемент замыкания является возвращаемым значением правила. То есть, правило factor возвращает double! Используя это, очень легко написать простой калькулятор на Spirit:
#include <boost/spirit/core.hpp>
#include <boost/spirit/attribute.hpp>
#include <iostream>
#include <string>
using namespace std;
using namespace boost::spirit;
using namespace phoenix;
struct calc_closure : boost::spirit::closure<calc_closure, double>
{
member1 val;
};
struct calculator : public grammar<calculator, calc_closure::context_t>
{
template <typename ScannerT>
struct definition
{
definition(calculator const& self)
{
top = expression[self.val = arg1];
expression
= term[expression.val = arg1]
>> *( ('+' >> term[expression.val += arg1])
| ('-' >> term[expression.val -= arg1])
)
;
term
= factor[term.val = arg1]
>> *( ('*' >> factor[term.val *= arg1])
| ('/' >> factor[term.val /= arg1])
)
;
factor
= ureal_p[factor.val = arg1]
| '(' >> expression[factor.val = arg1] >> ')'
| ('-' >> factor[factor.val = -arg1])
| ('+' >> factor[factor.val = arg1])
;
}
rule<ScannerT, calc_closure::context_t> expression, term, factor;
rule<ScannerT> top;
rule<ScannerT> const&
start() const { return top; }
};
};
Здесь используется еще один момент: параметризация замыканием всей грамматики, что позволяет дать грамматике возвращаемое значение. Здесь оно устанавливается в правиле top.
Более подробно о замыкания можно прочитать в соответствующей главе руководства Spirit.
