由于元旦第二天开始状态奇差,本周并没有增加太多内容,周记的内容也会相对少一些。以及本周的内容主要在于生成C++的代码,更多的是Ruby的元编程技巧。
指令定义
每个指令有一个InstType的枚举字段来标明类型
所有指令继承自一个VMInst类
1 | struct VMInst { |
C++解析
最主要的问题是要如何让C++解析这边生成的东西。我目前就选用了最简单粗暴的方法,直接生成字符串,用空格分离参数,用换行分离指令
获取所有指令信息
获取有哪些指令
我将所有的指令都放到了Rc::VM::Inst中,通过获取这个module的所有constant,判断哪些是Class
1 | def get_classes(mod) |
通过这个代码能够获取到Inst这个模块中的所有指令
- 获取每个指令里面是怎么样的
由于ruby并没有定义成员类型的东西,因此我选择自己造一个指定成员类型的东西
有两种实现
实现方式
TypeStruct
第一种是将Struct给包装一层,我给其命名为TypeStruct
使用方式
1 | class CondJump < TypeStruct.new(:cond, :addr => :int) |
类似于常规的Struct的使用方式,但是输入变成了可以是一个hash
实现
实现的一个要点在于new返回的东西需要是一个class。那么我们需要知道Ruby中new是怎么运作的
常规的对象来说,new中会做三件事。class MemberMap def initialize(type_defines) @type_defines = type_defines end def generate(c = “\n”, &f) @type_defines.generate(c, &f) end def keys @type_defines.map { |td| td.name } endend通过allocate分配空间,send initialize方法进行构造对象,最后将obj返回。而在这里只要修改返回的内容即可
另一个要点在于需要给返回的class添加一些实例方法
这里我们需要先理解常规的Struct.new做了什么,在我的理解本质上是返回了一个通过动态添加定义的匿名class,那么我们需要的是给这个匿名class添加一些方法来定义
那么我们很自然的就会想到将所有传给new的参数转换为每一个成员以及与之相应的类型定义,之后再对其中每一对“成员名⇒类型”定义对应的获取类型的方法
保存一个type_map,用于后面获取信息使用
来看一下代码
1 | def args_to_hash(*args) |
还有一个点是需要在这里检查type的合法性,这里想过生成类的,但是最后想或许现在没必要,还是先用符号吧。检查相关的代码如下
1 | module TypeCheck |
attr_type
第二种是增加了一个像attr_reader一样叫做attr_type的东西,但是这个要依赖于常规的Struct,我还是想要常规Struct内部的东西来避免重复代码。虽然有办法不依赖Struct,但是那样需要在这个attr_type里面引入更多不属于这个函数的功能,于是还是放弃吧
使用示例
1 | class Push < Struct.new(:value) |
实现
实现的核心原理还是参数转到hash再对每一对值define_method,只是这次我们要直接hack Module。attr_reader等函数也是采用的类似的做法
type_map的处置有一些不同,type_map需要将成员初始化,所有成员默认str类型,接着需要不断的merge新的参数,这个时候会将type_map中在args出现过的key所关联的值更新,这么解释可能比较复杂,看代码更直接一些
1 | {:a => 1}.merge({:a => 2}) |
1 | class Module |
二者的选择
最后的结果嘛…ide分析不出来,不想看到各种报错的红线。遇到需要手动new的时候只能改成第二种了
在获取成员的时候也用了很脏的做法,没找到什么在不new的情况下获取成员的好方法,因此也只有先new再从里面找。
生成
以前没做的坑
这里其实做一个dsl来描述然后生成是最好的。在好久之前了解rv的时候我甚至一度想开一个坑,用一个dsl来描述一个isa,之后生成对应的C++的读写代码。最后也是咕咕咕了,后续有时间可以做一下,还是挺有意思的。
这是一个描述load store的例子。当时做的时候没想到,现在一想其实也可以直接用Struct来描述,采用和我上面一致的方案
1 | ISA.define :LOAD do field :rd, 5 field :funct3, 3 field :rs1, 5 field :imm, 12endISA::define :STORE do field :offset_4_0, 5 field :width, 3 field :base, 5 field :src, 5 field :offset, 5end |
这是一个只做了外观没有做内部实现的例子,属实有点问题,正经人谁会搞出这玩意
1 | namespace :Suica do namespace :T do struct :F do auto :a1 auto :a2, 1 void :f2, ['a', 'b'] do end end endend |
生成的实现
有点扯远了,我们来看一下实际生成C++代码的部分。
我们需要生成如下几步
- 获取所有指令信息
- include头文件,名称空间等内容
- InstType的enum定义
- 所有指令类的定义
- 解析输入的部分
每个部分生成一个源码字符串,最后将这些拼接为一个长的字符串就好了
捋清这个流程以后就简单贴一下部分代码好了,源码中<<SRC的部分是一个字符串块的开始,SRC是结束,中间的任何字符都会保留,除了#{expr},这个是将expr to_s以后再嵌入进去
帮助方法
这是我自己加给Array的辅助函数,因为经常会有需要遍历array的所有对象做一套统一的操作最后再join连接的情况
1 | class Array |
demodulize_class的话就是简单的将类名去除了module前缀
获取所有指令信息
虽然上面提过,这里再放一下代码
1 | def get_classes(mod) |
头文件
1 | def gen_header_namespace |
InstType的enum定义
1 | def gen_enum_inst_type(classes) |
生成的样子
1 | enum class InstType { |
指令类定义
1 | def gen_class_define(klass) |
这里可能有一些需要提一下的东西,比如说有一个get_member_map
1 | class Class |
为了保持顺序,我选择了用数组来存放。指令最多无外乎一两百条,对于这个数据量不需要太去关心什么高效算法。
为了有更多的类型信息来帮助写易读和更可用的代码,一个名称类型对也转转换为了一个类型
1 | class TypeDefine < Struct.new(:name, :type) |
而MemberMap是一层包装,内部用typedefine的array存储,但也是可以像hash一样取出所有的key
1 | class MemberMap |
生成的样子
1 | struct Label : VMInst |
解析代码
1 | def gen_all_parser(classes) |
生成的样子(这里只放一个示例
1 | std::unique_ptr<VMInst> get_inst(const std::vector<std::string> &list) { |
C++代码格式
这里应该提一下,这种生成方式代码格式一定会乱七八糟,所以还应该调用一下clang-format处理一下。但是VM那边的clang-format之类的许多东西还没有加好,之后再做一下吧
最后
感谢你能看到这里,我再闲谈几句没什么关联的
这个系列我已经到了四篇,也就是一个月。持续做了这么几次已经可以确定只要不出意外自己就能连载下去,于是之后都会在推特推送我的更新(本周的就先算了,ruby本身所占比例有点大)RealAkemiHomura’ Twitter
如果对我的日常有兴趣可以点个关注,如果并不在意这个只想看后续的文章,那么可以通过rss订阅,或者每周一查看我的文章,更新一定是在周末
前面也提到元旦状态差,这些天甚至几次觉得这个系列过于玩具没有意义,想要断更、项目不想做下去了。但我最后还是决定继续更新,不为别的,只因为我还想接着做这个项目,哪怕内容如此简陋,只是一个过于简单的玩具,但我确实从中收获了知识和乐趣
- 本文标题:Rc-lang开发周记3 生成C++代码
- 本文作者:Homura
- 创建时间:2022-01-09 12:00:35
- 本文链接:https://homura.live/2022/01/09/rc-lang-dev/rc-lang-dev-3/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!