Clang-Repl

Clang-Repl 是一款交互式 C++ 解释器,支持增量编译。它以交互式的方式(即读-求值-打印-循环 (REPL) 方式)支持 C++ 的交互式编程。它使用 Clang 库将高级编程语言编译成 LLVM IR。然后,LLVM IR 由 LLVM 即时 (JIT) 基础设施执行。

Clang-Repl 适用于探索性编程以及需要快速获得洞察力的场景。Clang-Repl 是一个受 Cling 工作启发的项目,Cling 是一款基于 LLVM 的 C/C++ 解释器,由高能物理学领域开发,并由科学数据分析框架 ROOT 使用。Clang-Repl 允许将 Cling 的部分功能移到上游,使其对更广泛的受众有用且可用。

Clang-Repl 基本数据流

ClangRepl design

Clang-Repl 数据流可以大致分为 8 个阶段

  1. Clang-Repl 通过交互式提示或允许增量处理输入的接口控制输入基础设施。

  2. 然后,它将输入发送到 Clang 基础设施中的底层增量工具。

  3. Clang 将输入编译成 AST 表示。

  4. 如果需要,可以进一步转换 AST 以附加特定行为。

  5. 然后,将 AST 表示降低到 LLVM IR。

  6. LLVM IR 是 LLVM JIT 编译基础设施的输入格式。该工具将指示 JIT 运行指定函数,并将它们转换为针对底层设备架构(例如,Intel x86 或 NVPTX)的机器代码。

  7. LLVM JIT 将 LLVM IR 降低到机器代码。

  8. 然后执行机器代码。

构建说明:

$ cd llvm-project
$ mkdir build
$ cd build
$ cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DLLVM_ENABLE_PROJECTS=clang -G "Unix Makefiles" ../llvm

注意这里,上面的 RelWithDebInfo - 调试/发布

cmake --build . --target clang clang-repl -j n
   OR
cmake --build . --target clang clang-repl

Clang-repl 在 llvm-project/build/bin 下构建。进入目录 llvm-project/build/bin

./clang-repl
clang-repl>

Clang-Repl 使用方法

Clang-Repl 是一款交互式 C++ 解释器,支持增量编译。它以交互式的方式(即读-求值-打印-循环 (REPL) 方式)支持 C++ 的交互式编程。它使用 Clang 库将高级编程语言编译成 LLVM IR。然后,LLVM IR 由 LLVM 即时 (JIT) 基础设施执行。

基本:

clang-repl> #include <iostream>
clang-repl> int f() { std::cout << "Hello Interpreted World!\n"; return 0; }
clang-repl> auto r = f();
 // Prints Hello Interpreted World!
clang-repl> #include<iostream>
clang-repl> using namespace std;
clang-repl> std::cout << "Welcome to CLANG-REPL" << std::endl;
Welcome to CLANG-REPL
// Prints Welcome to CLANG-REPL

函数定义和调用:

clang-repl> #include <iostream>
clang-repl> int sum(int a, int b){ return a+b; };
clang-repl> int c = sum(9,10);
clang-repl> std::cout << c << std::endl;
19
clang-repl>

迭代结构:

clang-repl> #include <iostream>
clang-repl> for (int i = 0;i < 3;i++){ std::cout << i << std::endl;}
0
1
2
clang-repl> while(i < 7){ i++; std::cout << i << std::endl;}
4
5
6
7

类和结构体:

clang-repl> #include <iostream>
clang-repl> class Rectangle {int width, height; public: void set_values (int,int);\
clang-repl... int area() {return width*height;}};
clang-repl>  void Rectangle::set_values (int x, int y) { width = x;height = y;}
clang-repl> int main () { Rectangle rect;rect.set_values (3,4);\
clang-repl... std::cout << "area: " << rect.area() << std::endl;\
clang-repl... return 0;}
clang-repl> main();
area: 12
clang-repl>
// Note: This '\' can be used for continuation of the statements in the next line

Lambda 表达式:

clang-repl> #include <iostream>
clang-repl> using namespace std;
clang-repl> auto welcome = []()  { std::cout << "Welcome to REPL" << std::endl;};
clang-repl> welcome();
Welcome to REPL

使用动态库:

clang-repl> %lib print.so
clang-repl> #include"print.hpp"
clang-repl> print(9);
9

生成动态库

// print.cpp
#include <iostream>
#include "print.hpp"

void print(int a)
{
   std::cout << a << std::endl;
}

// print.hpp
void print (int a);

// Commands
clang++-17  -c -o print.o print.cpp
clang-17 -shared print.o -o print.so

注释:

clang-repl> // Comments in Clang-Repl
clang-repl> /* Comments in Clang-Repl */

关闭或终止:

clang-repl>%quit

与 Clang 一样,Clang-Repl 可以作为库(使用 clangInterpreter 库)集成到现有应用程序中。这将您的 C++ 编译器变成一个可以增量地使用和执行代码的服务。编译器即服务 (CaaS) 概念有助于支持高级用例,例如按需模板实例化和自动语言互操作性。它还有助于使 C/C++ 等静态语言更适合数据科学。

Clang-Repl 中的执行结果处理

下面讨论的执行结果处理功能通过在程序的执行结果和已编译程序之间创建接口来扩展 Clang-Repl 功能。

1. 捕获执行结果:此功能有助于捕获程序的执行结果并将它们带回已编译程序。

2. 转储捕获的执行结果:此功能有助于为值打印/自动 printf 创建临时转储,即显示捕获数据的数值和类型。

1. 捕获执行结果

在许多情况下,将程序执行结果带回已编译程序非常有用。此结果可以存储在类型为 Value 的对象中。

如何捕获执行结果(值合成):

合成器选择要合成的表达式,然后用合成的表达式替换原始表达式。根据表达式的类型,它可能会选择在为其分配内存时保存类型为 ‘value’ 的对象 (LastValue) (SetValueWithAlloc()),或不保存 (SetValueNoAlloc())。

Shows how an object of type 'Value' is synthesized

值合成

捕获的结果存储在哪里?

LastValue 保存值打印的最后结果。它是一个类成员,因为即使在后续输入之后也可以访问它。

注意:如果没有进行值打印,则它处于无效状态。

提高效率和用户体验

Value 对象本质上用于在表达式 ‘类型’ 和分配的 ‘内存’ 之间创建映射。内置类型(bool、char、int、float、double 等)是可复制的。它们的内存分配大小是已知的,Value 对象可以引入一个小缓冲区优化。对于对象,Value 类提供引用计数内存管理。

该实现将类型映射为编写后的类型和 Clang 类型,以便能够使用预处理器合成相关的强制转换操作。例如,X(char, Char_S),其中 char 是来自语言类型系统的类型,而 Char_S 是表示它的 Clang 内置类型。此映射有助于将执行结果从解释器导入已编译程序,反之亦然。Value.h 头文件可以在运行时包含,这就是为什么它的令牌数非常少,并且在开发时考虑了严格的约束。

这也使用户能够在代码中接收计算出的 ‘类型’,然后将类型转换为其他类型(例如,将双精度浮点数重新强制转换为单精度浮点数)。通常,编译器可以透明地处理这些转换,但在解释器模式下,编译器无法看到所有 ‘from’ 和 ‘to’ 类型,因此无法隐式地进行转换。因此,此逻辑能够根据请求提供这些转换。

根据请求进行转换有助于改善用户体验,当 ‘from’ 类型未知或不清楚时,允许转换到所需的 ‘to’ 类型。

此功能的重要性

‘Value’ 对象能够包装来自 JIT 的内存区域,并将它带回已编译的代码(反之亦然)。当

  • 将解释器连接到已编译的代码,或

  • 将解释器连接到另一种语言。

例如,此功能有助于跨边界传输值。一个值得注意的例子是 cppyy 项目代码利用此功能来启用在 Python 中运行 C++。它能够在 C++ 和 Python 之间传输值/信息。

注意:cppyy 是一款自动的运行时 Python 到 C++ 绑定生成器,用于从 Python 调用 C++,反之亦然。它使用 LLVM 以及 C++ 解释器(例如,Cling)来启用运行时 C++ 模板实例化、跨继承、回调、自动强制转换、透明使用智能指针等功能。

简而言之,此功能实现了一种新的代码开发方式,为语言互操作性和更轻松的交互式编程铺平了道路。

实现细节

解释器作为 REPL 与作为库

1 - 如果我们在交互式 (REPL) 模式下使用解释器,它将转储值(即值打印)。

if (LastValue.isValid()) {
  if (!V) {
    LastValue.dump();
    LastValue.clear();
  } else
    *V = std::move(LastValue);
}

2 - 如果我们将解释器用作库,那么它将把值传递给用户。

增量 AST 使用者

IncrementalASTConsumer 类包装原始代码生成器 ASTConsumer,并执行一个挂钩,遍历所有顶级声明,根据 isSemiMissing() 条件查找要合成的表达式。

如果发现此条件为真,则将调用 Interp.SynthesizeExpr()

注意:以下是示例代码片段。实际代码可能会随着时间的推移而改变。

for (Decl *D : DGR)
  if (auto *TSD = llvm::dyn_cast<TopLevelStmtDecl>(D);
      TSD && TSD->isSemiMissing())
    TSD->setStmt(Interp.SynthesizeExpr(cast<Expr>(TSD->getStmt())));

return Consumer->HandleTopLevelDecl(DGR);

然后,合成器将根据表达式的类型选择相关的表达式。

已编译代码和解释代码之间的通信

在 Clang-Repl 中,存在解释代码,而此功能添加了一个可以与已编译代码通信的 ‘value’ 运行时。

以下是一个已编译代码与解释器代码交互的示例。表达式的执行结果存储在类型为 Value 的对象 ‘V’ 中。然后,该值将被打印,从而有效地帮助解释器使用来自已编译代码的值。

int Global = 42;
void setGlobal(int val) { Global = val; }
int getGlobal() { return Global; }
Interp.ParseAndExecute(“void setGlobal(int val);”);
Interp.ParseAndExecute(“int getGlobal();”);
Value V;
Interp.ParseAndExecute(“getGlobal()”, &V);
std::cout << V.getAs<int>() << “\n”; // Prints 42

注意:以上是已编译代码和解释代码之间互操作性的示例。语言之间的互操作性(例如,C++ 和 Python)的工作方式类似。

2. 转储捕获的执行结果

此功能有助于创建临时转储以显示所需数据的数值和类型(漂亮的打印)。这是一种在交互式编程期间与解释器进行交互的好方法。

如何简化值打印(自动 printf)

Automatic Printf 功能使在程序执行期间轻松显示变量值。不需要反复使用 printf 函数。这是使用 libclangInterpreter 库中的扩展实现的。

要自动打印表达式的值,只需在全局范围内不带分号地编写表达式即可。

Shows how Automatic PrintF can be used

自动 PrintF

此功能的重要性

Cling 中类似实现的启发,此功能添加到上游 Clang 库中,本质上扩展了 C++ 的语法,使其对为数据科学应用程序编写代码的人员更有帮助。

例如,当您想尝试使用一组函数对一组值进行实验,并且您希望立即知道结果时,这很有用。这与 Python 的工作方式类似(因此 Python 在数据科学研究中很受欢迎),但是 C++ 的卓越性能以及这种灵活性使其成为更具吸引力的选择。

实现细节

解析机制:

Clang-Repl 中的解释器(Interpreter.cpp)包含函数 ParseAndExecute(),它可以接受一个“Value”参数来捕获结果。但如果该值参数是可选的并且被省略(即用户不想在其他地方使用它),则可以验证最后一个值并将其推入 dump() 函数。

Shows the Parsing Mechanism for Pretty Printing

解析机制

注意:以下是示例代码片段。实际代码可能会随着时间的推移而改变。

llvm::Error Interpreter::ParseAndExecute(llvm::StringRef Code, Value *V) {

auto PTU = Parse(Code);
if (!PTU)
    return PTU.takeError();
if (PTU->TheModule)
    if (llvm::Error Err = Execute(*PTU))
    return Err;

if (LastValue.isValid()) {
    if (!V) {
    LastValue.dump();
    LastValue.clear();
    } else
    *V = std::move(LastValue);
}
return llvm::Error::success();
}

dump() 函数(在 value.cpp 中)调用 print() 函数。

数据的打印和类型的打印分别在各自的函数中处理:ReplPrintDataImpl()ReplPrintTypeImpl()

注释令牌(annot_repl_input_end)

此功能使用一个新的令牌 (annot_repl_input_end) 来考虑打印表达式的值,如果它没有以分号结尾。当解析表达式语句时,如果最后一个分号丢失,则代码将假装有一个分号并在此处设置一个标记以供以后使用,然后继续解析。

在 C++ 中通常需要分号,但此功能扩展了 C++ 语法以处理预期缺少分号的情况(即,当处理表达式语句时)。它还确保在这种特定情况下不会为缺少的分号生成错误。

这是通过识别用户输入(表达式语句)的结束位置来实现的。这有助于有效地存储和返回表达式语句,以便可以将其打印(自动显示给用户)。

注意:此逻辑目前仅适用于 C++,因为部分实现本身需要 C++ 功能。未来版本可能会支持更多语言。

Token *CurTok = nullptr;
// If the semicolon is missing at the end of REPL input, consider if
// we want to do value printing. Note this is only enabled in C++ mode
// since part of the implementation requires C++ language features.
// Note we shouldn't eat the token since the callback needs it.
if (Tok.is(tok::annot_repl_input_end) && Actions.getLangOpts().CPlusPlus)
  CurTok = &Tok;
else
  // Otherwise, eat the semicolon.
  ExpectAndConsumeSemi(diag::err_expected_semi_after_expr);

StmtResult R = handleExprStmt(Expr, StmtCtx);
if (CurTok && !R.isInvalid())
  CurTok->setAnnotationValue(R.get());

return R;
  }

AST 转换

当 Sema 遇到 annot_repl_input_end 令牌时,它知道在实际 CodeGen 过程之前转换 AST。它将消耗该令牌并在相应的声明中设置一个“分号丢失”位。

if (Tok.is(tok::annot_repl_input_end) &&
    Tok.getAnnotationValue() != nullptr) {
    ConsumeAnnotationToken();
    cast<TopLevelStmtDecl>(DeclsInGroup.back())->setSemiMissing();
}

在 AST Consumer 中,遍历所有顶层声明,以查找要合成的表达式。如果当前声明是顶层语句声明 (TopLevelStmtDecl) 并且缺少分号,则要求解释器合成另一个表达式(内部函数调用)来替换此原始表达式。

详细的 RFC 和讨论:

有关这些功能的更多技术细节、社区讨论以及指向相关补丁的链接,请访问:LLVM Discourse 上的 RFC

RFC 中介绍的一些逻辑(例如 ValueGetter())可能已过时,与最终开发的解决方案相比。