本周主要是修复了之前C++代码生成的一些bug,之后开始搞函数定义与调用的部分。
函数解析方式
这里我一开始没想好怎么做的,所以会做的很诡异,最大的原因是静态类型语言和动态类型语言是不同的。由于我只对动态语言有一些了解,这里暂时只提动态语言的一些点
动态语言
手头动态类型语言的资料是相对较多的,而实际看编译出的产物也是相对熟悉一些。
对于Ruby和Python来说,函数都是动态定义的。因此解析到一个函数的时候会产生一个定义函数的指令
Ruby
1 | 0000 definemethod :foo, foo ( 1)[Li] |
(后面的1是行号)
Python
1 | def f(): |
而函数本体内容则是创建了一个函数对象并放到了其他的位置,以及地址是重新从0开始的。这个地址应该是相对地址,因为会动态装载
这两个的源代码不一样的,只是想展示地址都是从0开始。dump出来的内容差异也比较大
Ruby
1 | def foo |
1 | == disasm: #<ISeq:foo@<compiled>:9 (9,0)-(11,3)> (catch: FALSE) |
Python(函数体被编译成的内容
1 | def f(): |
1 | 0 LOAD_CONST 1 (“Function”) |
实现
一开始是想仿照做一个动态的实现,但是后来觉得还是静态的好,导致产生了如下的代码。
对于一个函数,我生成了一个DefineFun。FunLabel是因为我不知道它们是如何判断函数结尾到哪里的,这属于我当时的一个理解错误,编译的时候函数体的内容会被编译好放到其他位置,而不是说运行时再看到一个函数的标签,再将之后的一段代码跳过。
1 | # 只展示关键部分 |
正确的做法应当是在编译的时候就将这些代码单独放到其他位置,运行时再进行装载。
调用无参函数
函数调用我们先从简单的无参函数说起
1 | def f1 |
target
那么首先,我们需要考虑到call的target如何来做处理。很自然的会想到target可以使用字符串。
尽管使用字符串的话会导致指令长度膨胀,解析复杂等。但目前不考虑那些,解析的也是字符串指令,所以先这样
去哪里找目标函数的信息
这个自然来说是需要符号表中保存了
符号表中的函数信息
对于符号表来说,表中条目需要保存的信息有以下几条
- 参数个数(目前全部为无类型,因此返回类型也无需考虑)
- local变量的信息
- 函数体的指令地址
这些目前来说都是编译期间可知的,所以也会以字符串的方式dump出来供vm去解析。至于函数体地址的问题牵扯到链接,而目前我们先不需要考虑链接的情况,只需要将生成的符号表中的地址加载进来就好了。
生成符号表
由于以上需求,我们在编译的时候需要生成符号表信息
我们之前设计的全局符号表是这样的
1 | class GlobalEnv < Struct.new(:define_env,:const_table, :fun_env) |
暂时不考虑常量表,我们需要的是剩下两个表的信息。
生成vm指令这个阶段会将一个全局定义表(define_env,目前仅存其定义),将其定义更改为args以及offset
offset都是未知的所以先设置为一个未定义值,因为我是通过返回数组并且把数组连接起来的形式,所以这个时候并不知道偏移量。这里用一个数组存放值的做法实在很差劲,但是实在没精力改进了…先能跑吧
1 | def on_function(node) |
重新设置偏移量
1 | inst.each_with_index do |ins, index| |
而fun_env表,则是保存了每个表的参数以及局部变量的信息。拥有fun_env表和define_env表(这两个表其实应该合并,下次一定…)的信息,我们就能够生成出上面所需的信息了
1 | def gen_sym_table(global_env) |
生成示例 格式为 函数名,参数个数,local var个数,起始地址
1 | multi 2 2 0main 0 1 6 |
函数符号表中的条目
1 | struct FunInfo |
调用栈
既然要调用函数,那么就需要调用栈这个东西了
就目前的需求来说,调用栈中的栈帧需要有以下几种成员
- 前一个栈帧(跟踪整个调用链)
- 返回的pc地址(函数调用结束后需要返回到调用者)
- 当前栈帧在栈中的起始地址(起始地址开始分配局部变量的空间)
关于多个栈帧之间的存储方式,由于需要频繁添加删除尾部结点,因此选择了链表的方式。如果使用数组的话会牵扯到长度不够再重新分配数组空间的情况
而实际栈内数据的布局是
1 | ---------------- |
注意这里和实际的栈不同,对于实际的栈来说类似于返回的pc地址,以及前一个栈帧的地址都是保存在栈内的
返回值
目前的设计是返回值最后放到栈顶,这样返回的时候直接从栈顶取值,之后再恢复栈就可以了
调用带参数的函数
1 | def f1(a, b) |
参数传递
目前采用的是push的方式直接push参数,这个体现在函数调用的时候编译出的指令上
1 | def on_fun_call(fun_call) |
栈内数据排布
1 | ---------------- |
关于参数传递的话题其实还有很多,比如说顺序,变长参数,谁来释放,在之后的内容再一点点补足
正文无关闲谈
首先是最重要的一点:本周的内容就充满了各种应付式的内容,这在往期我都是会直接当场修改掉的,但实属有些无力…我在想这样的内容发出来会不会很不负责任,但是如果停更那我所做出的每周更新的承诺这么快就要被打破了,而且以后更容易不遵守了。
本周的内容相对少的多,最加对于压力的感知更加明显了,尽管我反复将注意力转移到当前做的事情上(每天也会有对应冥想练习),但很多事情依然力不从心。时间安排的太满,我不会的太多,但每一项我都无法舍弃,最后分配到做这个的时间真的不多了,还要一边查看各种实现学习一边写,好多东西都是周日写的时候才学习修改的。学习实现基本上也是靠看书,看前人总结过的内容,对于大型项目实在没有精力去扒。这周还在看Ruby的YJIT的论文,本就不多的时间更没多少了,最后论文也没看多少(就看了几段介绍…),这篇论文读明白后也会再出一篇博客,尽管只看了一点但也让我增加了许多JIT方面的常识
YJIT: a basic block versioning JIT compiler for CRuby
如何能摆脱这种状态,如果读者有经验还请赐教
如果我是学生的时候就能开始做这件事情就好了..可是没有那么多如果
- 本文标题:Rc-lang开发周记4 函数其一
- 本文作者:Homura
- 创建时间:2022-01-16 17:03:13
- 本文链接:https://homura.live/2022/01/16/rc-lang-dev/rc-lang-dev-4/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!