Как получить AST для строки регулярного выражения?

Как получить абстрактное синтаксическое дерево (AST) регулярного выражения (в С++)?

Например,

 (XYZ)|(123)

должно выводить дерево:

        |
      /   \
    .       .
   / \     / \
  .   Z   .   3    
 / \     / \   
X  Y     1 2

Существует ли грамматика boost::spirit для анализа шаблонов регулярных выражений? Библиотека boost::regex должна иметь это, но я ее не нашел. Существуют ли какие-либо другие доступные инструменты с открытым исходным кодом, которые дадут мне абстрактное представление регулярного выражения?

Ответы

Ответ 1

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

Итак, как обычно, я начал писать с ручкой и бумагой, и через некоторое время был принят ряд правил проекта. Время, чтобы провести аналогичный АСТ:

namespace ast
{
    struct multiplicity 
    {
        unsigned minoccurs;
        boost::optional<unsigned> maxoccurs;
        bool greedy;

        multiplicity(unsigned minoccurs = 1, boost::optional<unsigned> maxoccurs = 1) 
            : minoccurs(minoccurs), maxoccurs(maxoccurs), greedy(true)
        { }

        bool unbounded() const { return !maxoccurs; }
        bool repeating() const { return !maxoccurs || *maxoccurs > 1; }
    };

    struct charset
    {
        bool negated;

        using range   = boost::tuple<char, char>; // from, till
        using element = boost::variant<char, range>;

        std::set<element> elements; 
        // TODO: single set for loose elements, simplify() method
    };

    struct start_of_match {};
    struct end_of_match {};
    struct any_char {};
    struct group;

    typedef boost::variant<   // unquantified expression
        start_of_match,
        end_of_match,
        any_char,
        charset,
        std::string,          // literal
        boost::recursive_wrapper<group> // sub expression
    > simple;

    struct atom               // quantified simple expression
    {
        simple       expr;
        multiplicity mult;
    };

    using sequence    = std::vector<atom>;
    using alternative = std::vector<sequence>;
    using regex       = boost::variant<atom, sequence, alternative>;

    struct group {
        alternative root;

        group() = default;
        group(alternative root) : root(std::move(root)) { }
    };
}

Это ваш типичный AST (58 LoC), который хорошо работает с Spirit (из-за интеграции с boost с помощью variant и optional, а также со стратегически выбранными конструкторами).

Грамматика оказалась немного длиннее:

template <typename It>
    struct parser : qi::grammar<It, ast::alternative()>
{
    parser() : parser::base_type(alternative)
    {
        using namespace qi;
        using phx::construct;
        using ast::multiplicity;

        alternative = sequence % '|';
        sequence    = *atom;

        simple      = 
                      (group)
                    | (charset)
                    | ('.' >> qi::attr(ast::any_char()))
                    | ('^' >> qi::attr(ast::start_of_match()))
                    | ('$' >> qi::attr(ast::end_of_match()))
                    // optimize literal tree nodes by grouping unquantified literal chars
                    | (as_string [ +(literal >> !char_("{?+*")) ])
                    | (as_string [ literal ]) // lone char/escape + explicit_quantifier
                    ;

        atom        = (simple >> quantifier); // quantifier may be implicit

        explicit_quantifier  =
                    // bounded ranges:
                      lit('?')                                   [ _val = construct<multiplicity>( 0, 1)   ]
                    | ('{'  >> uint_ >> '}' )                    [ _val = construct<multiplicity>(_1, _1)  ]
                    // repeating ranges can be marked non-greedy:
                    | (                                        
                          lit('+')                               [ _val = construct<multiplicity>( 1, boost::none) ]
                        | lit('*')                               [ _val = construct<multiplicity>( 0, boost::none) ]
                        | ('{'  >> uint_ >> ",}")                [ _val = construct<multiplicity>(_1, boost::none) ]
                        | ('{'  >> uint_ >> "," >> uint_ >> '}') [ _val = construct<multiplicity>(_1, _2)  ]
                        | ("{," >> uint_ >> '}' )                [ _val = construct<multiplicity>( 0, _1)  ]
                      ) >> -lit('?')       [ phx::bind(&multiplicity::greedy, _val) = false ]
                    ;

        quantifier = explicit_quantifier | attr(ast::multiplicity());

        charset     = '[' 
                   >> (lit('^') >> attr(true) | attr(false)) // negated
                   >> *(range | charset_el)
                    > ']'
                    ;

        range       = charset_el >> '-' >> charset_el;

        group       = '(' >> alternative >> ')';

        literal     = unescape | ~char_("\\+*?.^$|{()") ;

        unescape    = ('\\' > char_);

        // helper to optionally unescape waiting for raw ']'
        charset_el  = !lit(']') >> (unescape|char_);
    }

  private:
    qi::rule<It, ast::alternative()>    alternative;
    qi::rule<It, ast::sequence()>       sequence;
    qi::rule<It, ast::atom()>           atom;
    qi::rule<It, ast::simple()>         simple;
    qi::rule<It, ast::multiplicity()>   explicit_quantifier, quantifier;
    qi::rule<It, ast::charset()>        charset;
    qi::rule<It, ast::charset::range()> range;
    qi::rule<It, ast::group()>          group;
    qi::rule<It, char()>                literal, unescape, charset_el;
};

Теперь самое интересное - сделать что-то с АСТ. Поскольку вы хотите визуализировать дерево, я подумал о создании графика DOT из AST. Итак, я сделал:

int main()
{
    std::cout << "digraph common {\n";

    for (std::string pattern: { 
            "abc?",
            "ab+c",
            "(ab)+c",
            "[^-a\\-f-z\"\\]aaaa-]?",
            "abc|d",
            "a?",
            ".*?(a|b){,9}?",
            "(XYZ)|(123)",
        })
    {
        std::cout << "// ================= " << pattern << " ========\n";
        ast::regex tree;
        if (doParse(pattern, tree))
        {
            check_roundtrip(tree, pattern);

            regex_todigraph printer(std::cout, pattern);
            boost::apply_visitor(printer, tree);
        }
    }

    std::cout << "}\n";
}

Эта программа приводит к следующим графикам:

enter image description here

Повторяется изображение с собственными краями, а цвет указывает, является ли совпадение жадным (красным) или неживым (синим). Как вы можете видеть, я немного оптимизировал AST для ясности, но (un) комментирование соответствующих строк будет иметь значение:

enter image description here

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

Полный код в этом контексте: https://gist.github.com/sehe/8678988

Ответ 2

Я считаю, что Boost Xpressive должен быть способен "почти" сделать это из коробки.

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

Я посмотрю, могу ли я подтвердить (с небольшим образцом).

Другие мысли включают использование Boost Spirit с универсальным средством для "хранения" AST. Вам нужно будет воспроизвести грамматику (что относительно просто для общих подмножеств синтаксиса Regex), поэтому это может означать большую работу.

Отчет о ходе выполнения 1

Глядя на Xpressive, я сделал несколько набегов. У меня есть красивые картинки с использованием графического отображения данных DDD. Но не достаточно.

Затем я больше изучил сторону "кода": Xpressive построен на Boost Proto. Он использует Proto для определения DSEL, который моделирует регулярные выражения непосредственно в коде С++. Прото генерирует дерево выражений (общий AST, если хотите) полностью в целом из кода С++ (путем перегрузки всех возможных операторов). В этом случае библиотека (Xpressive) должна определить семантику, пройдя дерево и, например,

  • построение дерева конкретных доменных имен
  • аннотирование/украшение его семантической информацией
  • возможно непосредственное семантическое действие (например, как Boost Spirit выполняет семантические действия в Qi и Karma 1)

Как вы можете видеть, небо действительно предел там, и все выглядит так же похоже на макросы компилятора, как в Boo, Nemerle, Lisp и т.д.


Визуализация выражений Trres

Теперь деревья выражений Boost Proto могут быть в целом визуализированы:

Работа с примером из Expressive С++: игра с синтаксисом Я немного расширил пример Xpressive "Hello World", чтобы отобразить дерево выражений:

#include <iostream>
#include <boost/xpressive/xpressive.hpp>
#include <boost/proto/proto.hpp>

using namespace boost::xpressive;

int main()
{
    std::string hello( "hello world!" );

    sregex rex = sregex::compile( "(\\w+) (\\w+)!" );

    // equivalent proto based expression
    rex = (s1= +_w) >> ' ' >> (s2= +_w) >> '!';
    boost::proto::display_expr( (s1= +_w) >> ' ' >> (s2= +_w) >> '!');

    smatch what;

    if( regex_match( hello, what, rex ) )
    {
        std::cout << what[0] << '\n'; // whole match
        std::cout << what[1] << '\n'; // first capture
        std::cout << what[2] << '\n'; // second capture
    }

    return 0;
}

Выход которого близок (обратите внимание на компилятор ABI конкретных имен typeid):

shift_right(
    shift_right(
        shift_right(
            assign(
                terminal(N5boost9xpressive6detail16mark_placeholderE)
              , unary_plus(
                    terminal(N5boost9xpressive6detail25posix_charset_placeholderE)
                )
            )
          , terminal( )
        )
      , assign(
            terminal(N5boost9xpressive6detail16mark_placeholderE)
          , unary_plus(
                terminal(N5boost9xpressive6detail25posix_charset_placeholderE)
            )
        )
    )
  , terminal(!)
)
hello world!
hello
world

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ. Вы должны понимать, что на самом деле это не показывает Regex AST, а скорее общее дерево выражений из Proto, поэтому оно лишено информации о конкретном домене (Regex). Я упоминаю это, потому что разница, вероятно, вызовет еще некоторую работу ( "если я не найду крючок в структурах компиляции Xpressive" ), чтобы она стала действительно полезной для исходного вопроса.

Что он на данный момент

Я останусь на этой ноте, как на обеденное время, и я собираю детей, но это, безусловно, привлекло мое внимание, поэтому я намерен опубликовать более позднее!


Выводы/отчет о проделанной работе 1.0000001

Плохие новости сразу: это не сработает.

Вот почему. Этот отказ был прав на деньги. Когда наступил выход, я уже много думал и "предсказал", что все это сломается прямо там, где я его оставил: AST основывается на дереве proto expression (а не на regex matchable_ex),

Этот факт был быстро подтвержден после некоторого контроля кода: после компиляции дерево proto expression больше не доступно для отображения. Не говоря уже о том, что basic_regex был задан как динамический шаблон в первую очередь (для него никогда не было прото-выражения).

Я был наполовину надеясь, что сопоставление было реализовано непосредственно на дереве прото-выражения (используя контексты эвакуляции/оценки прото), но быстро подтвердил, что это не так.

Итак, основной вынос:

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

Чуть менее строгие наблюдения включают

  • Boost Proto и Boost Expressive представляют собой очень интересные библиотеки (я не прочь там ловить рыбу). Я, очевидно, узнал несколько важных уроков о библиотеках мета-программирования шаблонов и, в частности, этих библиотеках.
  • Трудно создать парсер регулярных выражений, который строит статически типизированное дерево выражений. На самом деле это невозможно в общем случае - потребовалось бы, чтобы компилятор инстанцировал все возможные комбинации деревьев выражений на определенную глубину. Это, очевидно, не будет масштабироваться. Вы можете обойти это, введя полиморфный состав и используя полиморфный вызов, но это уберет преимущества метапрограммирования шаблонов (оптимизация времени компиляции для статически созданных типов/специализаций).
  • Как Boost Regex, так и Boost Expressive, вероятно, будут поддерживать какое-то регулярное выражение AST внутри (для поддержки оценки соответствия), но
    • он не был обнародован/задокументирован
    • нет очевидного средства отображения для этого

<суб > 1 Даже Spirit Lex поддерживает их, если на то пошло (но не по умолчанию) Суб >

Ответ 3

boost:: regex, похоже, имеет рукописный рекурсивный спуск парсер в basic_regex_parser.hpp. Несмотря на то, что это ужасно похоже на повторное изобретательство колеса, вы, вероятно, быстрее записываете грамматику в boost:: spirit самостоятельно, особенно с множеством форматов регулярных выражений.