学习内容
完成对 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 | Value *BinaryExprAST::codegen() { |
使用如下的方法编译llvm中的示例代码。
1 | cd llvm-project/llvm/examples/Kaleidoscope/Chapter9 |
然后用下面的测试代码测试toy程序(输入后ctrl+D结束程序)
1 | def binary , 1 (left right) right |
可以获得其输出的LLVM-IR打印如下。
1 | define double @te(double %y) !dbg !13 { |
可以看到te函数中的两个’,’ operator,其对应的行号都指向了rhs的位置,完全和源代码对不上。
使用我们的实现编译代码(因为已经内置了’,’,去掉了其定义)。
1 | extern kout(x) |
获得的输出如下。
1 | efine double @te(double %y) !dbg !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 | Expected no forward declarations! |
使用 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 | void setCallingConv(CallingConv::ID CC) { |
0值对应的意义可以在llvm/IR/CallingConv.h中找到
1 | /// A set of enums which specify the assigned numeric values for known llvm |
这样看起来,原文的说法是基本正确的。在没有设置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 |
|
输出的结果将是
1 | 0:1 |
而不是预期的1:1,2:2,3:3。并且,打开Wall -Wextra时也没有告警。。。
2
实现时再次测试了using namespace std;在头文件中的作用范围
1 |
|
会报如下错误。说明头文件中的using会污染和其相同的命名空间。控制using namespace的作用范围仍然是一个有意义的功能。
1 | tt.cpp:4:26: error: ‘string’ was not declared in this scope |