Kaleidoscope debug info

学习内容

完成对 https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/LangImpl09.html 的学习。

练习

看懂原网页后,实现同样的功能。

扩展

1 调试信息生成时,提到Kaleidoscope语言的abi接近C的abi。从哪里可以明确这一点?

2 source_loc的实现是否合理?有无改进空间?
string的存储?

3 能否将core_lib中的operator 先放到parser中解析,完成后再解析输入的用户文件?通过合并ast后再codegen,应该可以静默的实现语言扩展的operator。

4 var变量中的变量是否正确生成了调试信息

进展

为减少工作了,剔除原示例悄悄添加的ast dump功能(文字没有介绍,代码新增了)。
实现了debug信息的添加,并修复了原实现中的逻辑错误。

实现

实现中的主要改动如下:

1 出于节约工作量考虑,删除了原示例中的AST的dump功能,暂未实现;

2
原文实现时的调试信息发射有问题,会导致部分指令的调试信息错误。
如下binary op的发射代码所示,原示例在函数头部emitLocation,其信息会马上被随后的LHS/RHS codegen覆盖(他们也会emitLocation)。这样一来,真正属于operator的CreateFAdd指令会位于最后一个emitLocation指向的location(也就是RHS的location)。

1
2
3
4
5
6
7
8
9
10
11
12
13
Value *BinaryExprAST::codegen() {
KSDbgInfo.emitLocation(this);
......
Value *L = LHS->codegen();
Value *R = RHS->codegen();
if (!L || !R)
return nullptr;

switch (Op) {
case '+':
return Builder.CreateFAdd(L, R, "addtmp");
...
}

使用如下的方法编译llvm中的示例代码。

1
2
cd llvm-project/llvm/examples/Kaleidoscope/Chapter9
g++ toy.cpp -I ../include/ -I ../../../../../install/usr/local/include/ -L../../../../../install/usr/local/lib `../../../../../install/usr/local/bin/llvm-config --libs` -pthread -ldl -lz -ltinfo -fno-rtti -o toy

然后用下面的测试代码测试toy程序(输入后ctrl+D结束程序)

1
2
3
4
5
6
7
8
9
10
def binary , 1 (left  right) right

extern kout(x)
def te(y)
kout(y),
y=5,
kout(y)

def main()
te(2)

可以获得其输出的LLVM-IR打印如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
define double @te(double %y) !dbg !13 {
entry:
%y1 = alloca double
call void @llvm.dbg.declare(metadata double* %y1, metadata !17, metadata !DIExpression()), !dbg !18
store double %y, double* %y1
%y2 = load double, double* %y1, !dbg !19
%calltmp = call double @kout(double %y2), !dbg !19
store double 5.000000e+00, double* %y1, !dbg !20
%binop = call double @"binary,"(double %calltmp, double 5.000000e+00), !dbg !20
%y3 = load double, double* %y1, !dbg !21
%calltmp4 = call double @kout(double %y3), !dbg !21
%binop5 = call double @"binary,"(double %binop, double %calltmp4), !dbg !21
ret double %binop5, !dbg !21
}
...
!13 = distinct !DISubprogram(name: "te", scope: !2,
!20 = !DILocation(line: 6, column: 3, scope: !13)
!21 = !DILocation(line: 7, column: 6, scope: !13)

可以看到te函数中的两个’,’ operator,其对应的行号都指向了rhs的位置,完全和源代码对不上。
使用我们的实现编译代码(因为已经内置了’,’,去掉了其定义)。

1
2
3
4
5
6
7
8
9
extern kout(x)

def te(y)
kout(y),
y=5,
kout(y)

def main()
te(2)

获得的输出如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
efine double @te(double %y) !dbg !3 {
entry:
%y1 = alloca double
store double %y, double* %y1, !dbg !9
call void @llvm.dbg.declare(metadata double* %y1, metadata !8, metadata !DIExpression()), !dbg !10
%y2 = load double, double* %y1, !dbg !11
%callkout = call double @kout(double %y2), !dbg !12
store double 5.000000e+00, double* %y1, !dbg !13
%"_binary_,_with_prio_1" = call double @"_binary_,_with_prio_1"(double %callkout, double 5.000000e+00), !dbg !12
%y3 = load double, double* %y1, !dbg !14
%callkout4 = call double @kout(double %y3), !dbg !15
%"_binary_,_with_prio_15" = call double @"_binary_,_with_prio_1"(double %"_binary_,_with_prio_1", double %callkout4), !dbg !13
ret double %"_binary_,_with_prio_15", !dbg !13
}
...
!3 = distinct !DISubprogram(name: "te", scope: !1, file: !1, line: 3, type: !4, scopeLine: 3, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !7)
...
!12 = !DILocation(line: 4, column: 8, scope: !3)
!13 = !DILocation(line: 5, column: 4, scope: !3)

可以看到,修正后’,’的位置(参考)可以与源代码吻合。
%”binary,with_prio_1” = call double @”_binary,_with_prio_1”(double %callkout, double 5.000000e+00), !dbg !12 这里说明第一个’,’的location在!dbg !12中给出。
而!12 = !DILocation(line: 4, column: 8, scope: !3)准确给出了,’,’在代码的第4行第8列(scope: !3可以继续看到其属于te函数)。

3 为token也添加了location信息,ast的location从token中获取,不直接与lexer打交道层级更清晰。

4 新增了调试信息发射的控制流程和开关变量

5
在发射函数的IR时,我们的实现为了方便控制调试信息的发射,
将调试信息的发射拆分成了两块。args的调试信息在args的store指令之后发射。
这会导致verifyFunction是发生下面错误。

1
2
3
4
5
6
Expected no forward declarations!
!6 = <temporary!> !{}
store double %x, double* %x1, !dbg !7
store double %y, double* %y2, !dbg !7
call void @llvm.dbg.declare(metadata double* %x1, metadata !8, metadata !DIExpression()), !dbg !9
call void @llvm.dbg.declare(metadata double* %y2, metadata !10, metadata !DIExpression()), !dbg !9

使用 def foo (x y) x+y即可复现。
看起来dbg.declare需要放到对应store指令的前面。
参考https://stackoverflow.com/questions/34236034/how-to-track-down-llvm-verifyfunction-error-expected-no-forward-declarations/60656058#60656058后,
在添加verifyFunction前添加finalizeSubprogram,可更正错误。

扩展问题

1 调试信息生成时,提到Kaleidoscope语言的abi接近C的abi。从哪里可以明确这一点?
语言目前没有明确设计ABI,在LLVM-IR的生成过程中,其实也不需要配置这些内容。当需要具体生成代码时,LLVM的处理流程会用默认值来工作。对于函数的 calling conventions 来说,可以用setCallingConv方法来专门进行设置。通过追踪设置函数可以看到,其初始值应该为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  void setCallingConv(CallingConv::ID CC) {
auto ID = static_cast<unsigned>(CC);
assert(!(ID & ~CallingConv::MaxID) && "Unsupported calling convention");
setValueSubclassData((getSubclassDataFromValue() & 0xc00f) | (ID << 4));
}
--->
void setValueSubclassData(unsigned short D) {
Value::setValueSubclassData(D);
}
--->
void setValueSubclassData(unsigned short D) { SubclassData = D; }
--->
/// Hold arbitrary subclass data.
///
/// This member is defined by this class, but is not used for anything.
/// Subclasses can use it to hold whatever state they find useful. This
/// field iweizhis initialized to zero by the ctor.
unsigned short SubclassData;

0值对应的意义可以在llvm/IR/CallingConv.h中找到

1
2
3
4
5
6
7
8
9
/// A set of enums which specify the assigned numeric values for known llvm
/// calling conventions.
/// LLVM Calling Convention Representation
enum {
/// C - The default llvm calling convention, compatible with C. This
/// convention is the only calling convention that supports varargs calls.
/// As with typical C calling conventions, the callee/caller have to
/// tolerate certain amounts of prototype mismatch.
C = 0,

这样看起来,原文的说法是基本正确的。在没有设置ABI的情况下,LLVM应该是用了C的配置作为默认值。

2 source_loc的实现是否合理?有无改进空间?
为了简单,source_loc当前存在大量的冗余信息。
至少其中大量重复的filename string应该合并到一个上,改用idx指向一个vector。
最终的方案可能是参考gcc等成熟编译器,将source_loc整个设计为一个idx,要取用的时候再组装成完整的信息。内部储存时,可以合并冗余的string,甚至还可以进一步采用压缩编码方式来记录行号和列号(例如使用基础值+偏移值的方式来记录)。

3 能否将core_lib中的operator 先放到parser中解析,完成后再解析输入的用户文件?通过合并ast后再codegen,应该可以静默的实现语言扩展的operator。
可以,但是不能通过合并ast来实现。
当前用户自定义operator的功能需要lexer的支持,parse正式代码时lexer不知道新增了哪些operator,会导致unknown token的出现。
目前实现的方案是,在parser中静默导入了自定义operator的extern声明,这样用户可以像使用内置operator一样直接使用这些扩展operator。自定义operator的实现放到了core_support_lib中,封装脚本会链入实现。
当前实现的主要问题是,operator很多都是短小语句,应该inline优化的,但是拆开成库的形式后,只有lto优化才能达到效果。
后续可能的改进是,直接把def定义灌入parser,通过直接修改lexer的loc信息(或者先关掉调试信息输出生成operator定义部分,再codegen剩下的部分),解决调试信息的冲突问题。

4 var变量中的变量是否正确生成了调试信息
没有,原示例var和for中的变量都没有做declare,所以没有对应的调试信息。需要参考args中的处理方法,逐个添加。
出于工作量考虑,本次实现也暂时还未添加这些调试信息。

实现中遇到的问题

1 ranged loop 内部定义的变量,无法跨过循环体保存值。如下代码

1
2
3
4
5
6
7
8
#include <iostream>
using namespace std;
int main()
{
for (auto x : {1,2,3})
{int i = 0; cout << i++ << ":" << x <<endl;}
return 0;
}

输出的结果将是

1
2
3
0:1
0:2
0:3

而不是预期的1:1,2:2,3:3。并且,打开Wall -Wextra时也没有告警。。。

2
实现时再次测试了using namespace std;在头文件中的作用范围

1
2
3
4
#include <iostream>
namespace xx{using namespace std;}
namespace xx{void tt() {string x;}}
namespace xx1{void tt() {string x;}}

会报如下错误。说明头文件中的using会污染和其相同的命名空间。控制using namespace的作用范围仍然是一个有意义的功能。

1
2
tt.cpp:4:26: error: ‘string’ was not declared in this scope
namespace xx1{void tt() {string x;}}