19

我目前正在编写一个位于 C++ 解释器之上的程序。用户在运行时输入 C++ 命令,然后将其传递给解释器。对于某些模式,我想用修改后的形式替换给出的命令,以便我可以提供额外的功能。

我想替换任何形式的东西

A->Draw(B1, B2)

MyFunc(A, B1, B2).

我的第一个想法是正则表达式,但这很容易出错,因为任何A, B1, 或B2可能是任意 C++ 表达式。由于这些表达式本身可能包含带引号的字符串或括号,因此很难用正则表达式匹配所有情况。此外,此表达式可能有多种嵌套形式

我的下一个想法是将 clang 作为子进程调用,使用“-dump-ast”来获取抽象语法树,对其进行修改,然后将其重建为要传递给 C++ 解释器的命令。但是,这需要跟踪任何环境更改,例如包含文件和前向声明,以便为 clang 提供足够的信息来解析表达式。由于解释器不公开此信息,因此这似乎也不可行。

第三个想法是使用 C++ 解释器自己的内部解析来转换为抽象语法树,然后从那里构建。然而,这个解释器并没有以我能找到的任何方式暴露 ast。

是否有任何关于如何进行的建议,无论是沿着规定的路线之一,还是完全沿着不同的路线?

4

6 回答 6

3

你想要的是一个程序转换系统。这些工具通常可以让您表达对源代码的更改,以源代码级别的模式编写,基本上说:

 if you see *this*, replace it by *that*

但是在抽象语法树上操作,因此匹配和替换过程比字符串黑客得到的更值得信赖。

此类工具必须具有用于感兴趣的源语言的解析器。源语言是 C++ 使得这相当困难。

Clang有点资格;毕竟它可以解析C++。OP 对象在没有所有环境上下文的情况下无法这样做。就 OP 在解释器中输入(格式良好的)程序片段(语句等)而言,Clang 可能 [我自己对此没有太多经验] 难以专注于片段是什么(语句? 表达式? 声明? ...)。最后,Clang 并不是真正的 PTS。它的树修改过程不是源到源的转换。这对方便很重要,但可能不会阻止 OP 使用它;表面语法重写规则很方便,但您总是可以更加努力地替换程序树黑客。当有多个规则时,这开始变得很重要。

具有 Melt 的 GCC 与Clang 的资格相同。我的印象是 Melt 最多让 GCC 对这种工作不那么难以忍受。YMMV。

我们的DMS 软件再造工具包及其完整的 C++14 [编辑 2018 年 7 月:C++17] 前端绝对合格。DMS 已被用于对大规模 C++ 代码库进行大规模转换。

DMS 可以解析 C++ 的任意(格式正确)片段,而无需事先告知语法类别是什么,并使用其模式解析机制返回正确语法非终结符类型的 AST。[您可能会遇到多个解析,例如歧义,您将决定如何解决,请参阅Why can't C++ be parsed with a LR(1) parser? 更多讨论] 如果您愿意在解析时不使用宏扩展,并且坚持预处理器指令(它们也被解析)相对于代码片段(#if foo{#endif not allowed) 但这对于交互式输入的代码片段来说不太可能是一个真正的问题。

然后,DMS 提供了一个完整的过程 AST 库来操作解析的树(搜索、检查、修改、构建、替换),然后可以从修改的树中重新生成表面源代码,将 OP 文本提供给解释器。

在这种情况下,它的亮点在于 OP 可以将他的大部分修改直接写为源到源语法规则。对于他的示例,他可以为 DMS 提供重写规则(未经测试但非常接近正确):

rule replace_Draw(A:primary,B1:expression,B2:expression):
        primary->primary
    "\A->Draw(\B1, \B2)"     -- pattern
rewrites to
    "MyFunc(\A, \B1, \B2)";  -- replacement

在将匹配项替换为 A、B1 和 B2 之后,DMS 将采用任何包含左侧“...绘制...”模式的已解析 AST 并用右侧替换该子树。引号是元引号,用于区分 C++ 文本和规则语法文本;反斜杠是在元引号内用于命名元变量的元转义。有关您可以在规则语法中表达的内容的更多详细信息,请参阅DMS 重写规则

如果 OP 提供了一这样的规则,则可以要求 DMS 应用整个规则。

所以我认为这对OP来说效果很好。这是一个相当重量级的机制,可以“添加”到他想要提供给第 3 方的包中;DMS 及其 C++ 前端几乎不是“小”程序。但是现代机器有很多资源,所以我认为这是一个问题,即 OP 需要做多糟糕的事情。

于 2016-01-28T09:20:09.077 回答
0

尝试修改标题以抑制方法,然后编译您会发现错误并且能够替换所有核心。

至于你有一个 C++ 解释器(作为 CERN 的根),我想你必须使用编译器来拦截所有的 Draw,一个简单而干净的方法是在头文件中将 Draw 方法声明为私有,使用一些定义

 class ItemWithDrawMehtod
 {
 ....
 public:
 #ifdef CATCHTHEMETHOD
     private:
 #endif
 void Draw(A,B);
 #ifdef CATCHTHEMETHOD
     public:
 #endif
 ....
 };

然后编译为:

 gcc -DCATCHTHEMETHOD=1 yourfilein.cpp
于 2015-09-06T17:40:16.167 回答
0

当有人得到 Draw 成员函数 ( auto draw = &A::Draw;) 然后开始使用时会发生draw什么?大概您也希望在这种情况下调用相同的改进绘图功能。因此,我认为我们可以得出结论,您真正想要的是Draw用您自己的函数替换成员函数。

由于您似乎无法Draw直接修改包含的类,因此解决方案可能是从中派生您自己的类A并在其中覆盖Draw。然后你的问题就减少到让你的用户使用你新改进的类。

您可能会再次考虑将类的使用自动转换A为新的派生类的问题,但是如果没有完整的 C++ 实现的帮助,这似乎仍然相当困难。也许有一种方法可以A通过巧妙地使用头文件来隐藏旧定义并以该名称显示您的替换,但我无法确定您告诉我们的情况是否如此。

另一种可能性可能是使用一些使用 LD_PRELOAD 的动态链接器hackery 来替换Draw在运行时调用的函数。

于 2015-12-19T13:53:39.560 回答
0

可能有一种方法可以主要使用正则表达式来完成此操作。

由于 Draw( 之后出现的任何内容都已正确格式化为参数,因此您无需完全解析它们以达到您概述的目的。

从根本上说,重要的部分是“SYMBOL->Draw(”

SYMBOL 可以是解析为重载对象的任何表达式 -> 或解析为实现 Draw(...) 的类型的指针。如果将其减少到两种情况,则可以缩短解析。

对于第一种情况,搜索任何有效 C++ 符号的简单正则表达式,类似于“[A-Za-z_][A-Za-z0-9_\.]”,以及文字表达式“->Draw ("。这将为您提供必须重写的部分,因为此部分后面的代码已被格式化为有效的 C++ 参数。

第二种情况适用于返回重载对象或指针的复杂表达式。这需要更多的努力,但是可以轻松地编写一个简短的解析例程来向后遍历一个复杂的表达式,因为您不必支持块(C++ 中的块不能返回对象,因为 lambda 定义不调用lambda 本身和实际的嵌套代码块 {...} 不能直接返回任何适用于此处的内联代码)。请注意,如果表达式不以 ) 结尾,那么它在此上下文中必须是有效符号,因此如果您发现 a ) 只需将嵌套的 ) 与 ( 匹配并提取嵌套 SYMBOL(...(.. .)...)->Draw() 模式。这可以通过正则表达式实现,但在普通代码中也应该相当容易。

一旦有了符号或表达式,替换就很简单了,从

符号->绘制(...

你的功能(符号,...

无需处理 Draw() 的附加参数。

作为一个额外的好处,使用此模型可以免费解析链式函数调用,因为您可以递归地迭代代码,例如

A->Draw(B...)->Draw(C...)

第一次迭代识别第一个 A->Draw( 并将整个语句重写为

YourFunction(A, B...)->Draw(C...)

然后用它前面的表达式“YourFunction(A, ...)->”标识第二个 ->Draw,并将其重写为

YourFunction(YourFunction(A, B...), C...)

其中 B... 和 C... 是格式良好的 C++ 参数,包括嵌套调用。

在不知道您的解释器支持的 C++ 版本或您将要重写的代码类型的情况下,我真的无法提供任何可能值得的示例代码。

于 2016-01-28T06:45:32.773 回答
0

如果用户想向应用程序输入复杂的算法,我建议将脚本语言集成到应用程序中。这样用户就可以编写代码[以定义的方式执行函数/算法],以便应用程序可以在解释器中执行它并获得最终结果。例如:Python、Perl、JS 等。

由于您在解释器中需要 C++,因此建议http://chaiscript.com/ 。

于 2015-09-16T13:51:20.223 回答
-1

一种方法是将用户代码加载为 DLL(类似于插件),这样,您无需编译实际应用程序,只需编译用户代码,您的应用程序将动态加载它。

于 2016-02-16T13:22:24.723 回答