вторник, 15 февраля 2011 г.

Дух BOOST’a. Spirit


Когда дело доходит до  написания парсера в первую очередь всплывает в памяти Bizon i YACC. Spirit – это еще один замечательный инструмент. Отличием которого является возможность описания грамматики прямо в С++ коде.

double_ -- вот так выглядит парсер одного числа.

           Теперь попробуем написать парсер который будет разбирать числа, написанные через запитую, причем мы знаем, что первое число имеет тип int, второе и третье double.

+(int_>>’,’>> double_’,’>> double_>>-’,’) – на первый взгляд уже сложнее, но поверьте через несколько секунд, впечатление изменится.


  • int_ как и double_ парсеры соответствующих типов.
  • Оператор >> это разделитель отдельных парсеров, а также он говорит, что за ним еще что-то следует.
  • ’,’ – парсер запятой. В одинарные кавычки может быть вписан любой символ, только следует иметь ввиду что писать один за другим несколько таких парсеров разделенных оператором  >> нельзя.
  • Оператор + говорит о том, что конструкция, которая следует за ним, может повторяться сколько угодно раз, но обязательно не меньше чем один раз.
  • Оператор - означает, что в данном случае символ ’,’ может быть, а может и не быть. 
И так теперь мы можем без проблем распарсить строку вида «12, 13.45, 45.8, 4, 45.9, 78.0».

std::string num = “12, 13.45, 45.8, 4, 45.9, 78.0”;

bool r = phrase_parse(
        num.begin(),                          
        num.end(),                           
        +(int_>>’,’>> double_’,’>> double_>>-’,’),
        space                           
    );
Продолжаем
       Теперь в качестве примера рассмотрим парсер С++ кода, который выделяет из кода классы унаследованные от Widget, и заполняет соответствующую структуру. Она имеет вид: свойства, и виртуальные функции. Функции в свою очередь структуры, в которые сохраняются возвращаемый тип данных, имя функции, список типов, получаемых функцией.
Но сначала разберемся с тем, что такое rules.
      Rules – это не большие правила разбора текста, которые легче воспринимаются человеком. Которые потом объединяются. Основной задачей является повышение удобства использования.
qi::rule<iterator, std::string()=""> name;
name = lexeme[(qi::alpha | '_') >> *(qi::alnum | '_')];

      И так правило name означает лексему которая начинается с буквы, либо символа нижнего подчеркивания а за тем может быть сколько угодно букв, цифр или ’_’. 
  • Новый оператор | означает логический оператор или.
  • Директива  lexeme[] нужна для того чтобы по символу разобрать строку, невзирая на то правило вызывается в функции phrase_parse, которая в свою очередь уже разбивает строку на слова. Для этого в примере и передаем четвертым аргументом разделитель ascii::space_type.
qi::rule<Iterator, std::string(), ascii::space_type> type;

type = *(qi::string("unsigned") | qi::string("signed") | qi::string("const") | qi::string("long")) >> name >> *(qi::string("::") >> name) >> *(char_('*') | char_('&'));
      Правило type уже сложнее в своей структуре. Описывает такие типы как const float&, или long long int.
  • Оператор * говорит о том, что конструкция, которая следует за ним, может повторяться сколько угодно раз, а может и вообще не встречаться.
*(qi::string("unsigned") | qi::string("signed") | qi::string("const") | qi::string("long")) -- означает что в начале типа может быть const, или long. Причем long может встречаться несколько раз. После этого обязательно должно сработать правило name. Вариант когда через оператор :: записывается название области видимости предусмотрен так: *(qi::string("::") >> name). И поскольку это может быть ссылка или указатель в конце разбираем *(char_('*') | char_('&')).
          Имея правила для описания типов и имен уже легко можно описать нужные нам функции, но имеется одно но. Нам нужно заполнять структуру соответствующими данными. Для этого используем Boost.Fusion.
    struct method_struct
     
    struct method_struct
        {
            std::string ret_type;
            std::string name;
            std::vector<std::string> in_type;
        };
    BOOST_FUSION_ADAPT_STRUCT(
        method_struct,
        (std::string, ret_type)
        (std::string, name)
        (std::vector<std::string>, in_type)
    )
    qi::rule<iterator, ascii::space_type, method_struct()> method;
    
    method = "virtual">> type[at_c<0>(_val) = _1] >> name[at_c<1>(_val) = _1] >> '(' >> *(type[push_back(at_c<2>(_val), _1)] >>omit[-name>>-lit(',')])>>')'>>omit[-body];
    




          Разберем эту часть type[at_c<0>(_val) = _1].  at_c<0>(_val) = _1 означает: сохранить _1 в элемент структуры с номером 1. Таким образом выражение описанное в квадратных скобках заполняет нулевой элемент структуры данными полученными правилом type, так как _1 хранит это значение.
          Почти тоже самое и здесь. [push_back(_val), _1)]. push_back служит для заполнения векторов. То что правило возвращает структуру method_struct показано в объявлении третьим параметром.

    Сохранение данных 
            Есть несколько способов сохранять значения которые возвращают правила. Вот один из них. method [std::bind(&ClassWrapper::setFunction, &wrap, std::placeholders::_1) ]. Метод  ClassWrapper::setFunction(const &method_struct) сохраняет значение нужным образом. Использовать bind здесь очень удобно.

    Заключение 
          На мой взгляд самые сложные места разобраны. Привожу полную грамматику и функцию, которая осуществляет разбор. Конечно же здесь рассмотрено далеко не все, но help нам поможет.

    namespace client
     {
         namespace qi = boost::spirit::qi;
         namespace ascii = boost::spirit::ascii;
    
         template <typename Iterator>
         bool parse_class(Iterator first, Iterator last, ClassWrapper& wrap)
         {
            using qi::char_;
            using qi::_val;
            using qi::phrase_parse;
            using qi::lexeme;
            using qi::_1;
    
           namespace fusion = boost::fusion;
           namespace phoenix = boost::phoenix;
    
           using qi::lit;
           using ascii::space;
           using boost::spirit::qi::omit;
           using boost::spirit::qi::alpha;
           using phoenix::at_c;
           using phoenix::push_back;
    
          qi::rule<Iterator, std::string(), ascii::space_type> class_name;
          qi::rule<Iterator, std::string()> class_properties;
          qi::rule<Iterator, method_struct(), ascii::space_type> method;
          qi::rule<Iterator, std::string(), ascii::space_type> type;
          qi::rule<Iterator, std::string()> name;
          qi::rule<Iterator, std::string(), ascii::space_type> data_type;
          qi::rule<Iterator, std::string(), ascii::space_type> body;
          qi::rule<Iterator, std::string(), ascii::space_type> comment;
    
          class_name = omit[*(char_ - "class")] >> "class">> name >> ":" >> lit("public") >> lit("Widget");
    
          class_properties = lit("Q_PROPERTY")>>'('>>*(char_ - ')')>>lit(')');
    
          type = *(qi::string("unsigned") | qi::string("signed") | qi::string("const") | qi::string("long")) >>-qi::string("long") >> name >> *(qi::string("::") >> name) >> *(char_('*') | char_('&'));
    
          name = lexeme[(qi::alpha | '_') >> *(qi::alnum | '_')];
    
          method = "virtual">> type[at_c<0>(_val) = _1] >> name[at_c<1>(_val) = _1] >> '(' >> *(type[push_back(at_c<2>(_val), _1)] >>omit[-name>>-lit(',')])>>')'>>omit[-body];
    
          data_type = (qi::string("public") | qi::string("protected") | qi::string("private"))[_val += _1]>>-qi::string("slots")[_val += _1]>>lit(':');
    
          body = lit('{')>> *(char_ - '}')>>'}';
    
          comment = qi::no_skip[(qi::string("/*") >> *(char_ - "*/") >> "*/") | "//">>(*(char_ - qi::eol) >> qi::eol) ] ;
    
    
           bool r = qi::phrase_parse(first, last,
           (
           *(( class_name[std::bind(&ClassWrapper::setName, &wrap, std::placeholders::_1)]
           >> '{'
           >>+(qi::space
                 | data_type [std::bind(&ClassWrapper::setFuncType, &wrap, std::placeholders::_1)]
                 | body
                 | comment
                 | class_properties[std::bind(&ClassWrapper::setProperty, &wrap, std::placeholders::_1)]
                 | method [std::bind(&ClassWrapper::setFunction, &wrap, std::placeholders::_1)]
                 | (char_ - '}'))
           >> '}'
           >>*char_) | char_ )
          ),space
          ) ;
    
          if(first!=last)
            return false;
          return r;
        }
    
     }
    
          Надеюсь Spirit понравится вам также как и мне. И вы будете получать удовольствие от его использования.





    2 комментария:

    1. В принципе идея понятна. Несмотря, что с boost я знаком мало, да и на С++ не писал уже давно.
      Кстати ты слышал о таких способах парсинга как Regular Expressions, PCRE? Они пришли из Перла и активно внедрились в Java, C#, Python, etc. С++, и в частности Qt похоже пошли своим путём. На первый вгляд С++ boost fusion требует больше тексту сравнительно с тем же питоном или даже джавой. Дает ли это реальные плюсы?
      Просто интересно сравнить их было в плане быстродействия например. Как бы намекаю на тему новой статьи..

      ОтветитьУдалить
    2. Обсуждение этой темы являются холиваром для многих веток форумов и блогов. На мой взгляд они созданы немного для разных целей. Ведь классические регулярные выражения являются конечным автоматом. А вставка рекурсии в свою очередь отходит от базовых принципов.
      Если говорить о количестве текста для описания грамматики не стоит забывать и о преимуществах таких языков как Lua.
      Преимуществом Spirit'a является возможность встраивать парсеры в С++ код. Также он очень хорошо спроектирован, примером является определение символов римской системы исчисления.
      struct ones_ : qi::symbols
      {
      ones_()
      {
      add
      ("I" , 1)
      ("II" , 2)
      ("III" , 3)
      ("IV" , 4)
      ("V" , 5)
      ("VI" , 6)
      ("VII" , 7)
      ("VIII" , 8)
      ("IX" , 9) ;
      }
      } ones;(http://www.boost.org/doc/libs/1_46_0/libs/spirit/doc/html/spirit/qi/tutorials/roman_numerals.html)
      Такой парсер сразу вернет нужное число. Не представляю себе что-нибудь похожее на том же Python. Хотя он в определенных случаях может оказаться удобнее.

      ОтветитьУдалить