【问题标题】:How to get the AST of a regular expression string?如何获取正则表达式字符串的 AST?
【发布时间】:2011-12-01 05:02:51
【问题描述】:

如何获得正则表达式的抽象语法树 (AST)(在 C++ 中)?

例如,

 (XYZ)|(123)

应该产生一棵树:

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

是否有 boost::spirit 语法来解析正则表达式模式? boost::regex 库应该有,但我没找到。是否有任何其他可用的开源工具可以为我提供正则表达式的抽象表示?

【问题讨论】:

    标签: c++ regex boost


    【解决方案1】:

    我又一次偶然发现了这个问题。我决定看看使用 Boost Spirit 为正则表达式语法的重要子集编写解析器实际上是多么困难。

    所以,像往常一样,我从笔和纸开始,过了一段时间,我有了一些规则草案。是时候画出类似的 AST了:

    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 配合得很好(由于通过 variantoptional 与 boost 集成,以及具有战略性选择的构造函数)。

    语法最后只是稍微长了一点:

    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;
    };
    

    现在,真正有趣的是用 AST 做点什么。由于您想可视化树,我想到了从 AST 生成 DOT 图。所以我做了:

    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";
    }
    

    该程序产生以下图表:

    自边缘描绘重复,颜色表示匹配是贪婪(红色)还是非贪婪(蓝色)。如您所见,为了清晰起见,我对 AST 进行了一些优化,但是(取消)注释相关行会有所不同:

    我认为调整不会太难。希望它会成为某人的灵感。

    此要点的完整代码:https://gist.github.com/sehe/8678988

    【讨论】:

    • +1 表示受虐狂。我什至没有勇气使用正则表达式,更不用说为它们编写解析器...
    • 感谢您提供这段代码 - 这是我对 Boost.Spirit 的核心介绍,现在我正在用它来完成我自己的项目......
    • @DanNissenbaum 很高兴听到这个消息。神速!
    【解决方案2】:

    我认为 Boost Xpressive 必须能够“几乎”开箱即用地做到这一点。

    xpressive is an advanced, object-oriented regular expression template library for C++. Regular expressions can be written as strings that are parsed at run-time, or as expression templates that are parsed at compile-time. Regular expressions can refer to each other and to themselves recursively, allowing you to build arbitrarily complicated grammars out of them.

    我看看能不能确认(用小样本)。

    其他想法包括使用带有通用 utree 工具的 Boost Spirit 来“存储”AST。您必须重现一个语法(这对于 Regex 语法的常见子集来说相对简单),所以这可能意味着更多的工作。

    进度报告 1

    看着 Xpressive,我取得了一些进展。我使用 DDD 出色的图形数据显示获得了一些漂亮的图片。但还不够漂亮。

    然后我进一步探索了“代码”方面:Xpressive 是基于 Boost Proto 构建的。它使用 Proto 来定义一个直接在 C++ 代码中对正则表达式建模的 DSEL。 Proto 完全从 C++ 代码(通过重载所有可能的运算符)生成表达式树(通用 AST,如果你愿意的话)。然后,库(在本例中为 Xpressive)需要通过 遍历树 来定义语义,例如

    • 构建特定领域的表达式树
    • 用语义信息注释/装饰它
    • 可能直接采取语义行动(例如 Boost Spirit 如何在 Qi 和 Karma 中执行语义行动1

    如您所见,天空真的是极限,而且看起来与 Boo、Nemerle、Lisp 等编译器宏非常相似。


    可视化表达式 Trres

    现在,Boost Proto 表达式树可以一般地可视化

    根据Expressive C++: Playing with Syntax 的示例,我稍微扩展了 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;
    }
    

    其输出接近(注意compiler 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
    

    免责声明您应该意识到这实际上并不是显示正则表达式 AST,而是来自 Proto 的 通用表达式树,因此它没有特定于域的 (Regex)信息。我提到它是因为差异可能会导致更多的工作(?除非我找到 Xpressive 的编译结构的挂钩)才能真正对原始问题有用。

    到此为止

    我会留下那张纸条,因为现在是午餐时间,我要去接孩子们,但这确实引起了我的兴趣,所以我打算稍后再发更多!


    结论/进度报告 1.0000001

    马上就有坏消息:它不起作用。

    原因如下。那个免责声明是正确的。当周末到来时,我已经在考虑更多事情并“预测”整个事情会在我离开的地方崩溃:AST 是基于 proto 表达式树(不是正则表达式 matchable_ex)。

    经过一些代码检查后,这一事实很快得到证实:编译后,proto 表达式树不再可用,无法显示。更不用说 basic_regex 最初被指定为动态模式(从来没有它的原型表达式)。

    我一直一半希望匹配已直接在原型表达式树上实现(使用原型评估/评估上下文),但很快证实事实并非如此。

    所以,主要的收获是:

    • 这对于显示任何正则表达式 AST 都不起作用
    • 您可以用上述方法做的最好的事情是可视化一个 proto 表达式,您必须直接在代码中创建它。这是在同一代码中手动编写 AST 的一种奇特方式...

    稍微不那么严格的观察包括

    • Boost Proto 和 Boost Expressive 是非常有趣的库(我不介意去那里钓鱼)。我显然学到了一些关于模板元编程库的重要课程,尤其是这些库。
    • 很难设计一个构建静态类型表达式树的正则表达式解析器。事实上,在一般情况下这是不可能的——它需要编译器将所有可能的表达式树组合实例化到一定深度。这显然不会扩展。您可以通过引入多态组合和使用多态调用来解决这个问题,但这会消除模板元编程的好处(静态实例化类型/专业化的编译时优化)。
    • Boost Regex 和 Boost Expressive 都可能在内部支持某种正则表达式 AST(以支持匹配评估),但
      • 尚未公开/记录
      • 没有明显的显示设施

    1 即便是 Spirit Lex 也支持它们(但默认情况下不支持)

    【讨论】:

    • 添加了一个小的免责声明,以防止任何误解。这是正在进行的工作。但它显示出真正的潜力迹象,IMO
    • gah - 等到周末继续这个任务;当然,那很快也是路的尽头……事后看来,我应该预见到这一点。我用结论更新了答案。干杯!
    • 我很确定您需要做的就是使用 Xpressive 语法转换原始 AST 以获得正确的 Xpressive AST - 在 Xpressive 代码库中查找对 proto::transform 的调用。周末我会帮忙调查一下,但我家里的显卡现在已经坏了。 :-[
    【解决方案3】:

    boost::regex 似乎在 basic_regex_parser.hpp 中有一个手写的递归下降解析器。尽管感觉非常像重新发明轮子,但您自己在 boost::spirit 中编写语法时可能会更快,尤其是在周围有大量正则表达式格式的情况下。

    【讨论】:

    • 我刚刚done just that。然而,公平的警告:我正在用语法变化来触及表面。 (不过,我已经很好地测试了支持的语法)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-07
    • 1970-01-01
    • 2015-05-21
    相关资源
    最近更新 更多