原文链接:http://www.hokstad.com/writing-a-compiler-in-ruby-bottom-up-step-4.html
抱歉,又拖了很长时间。要忙的事情实在很多。正如上一篇文章末尾提到的那样,这次要讲的是自定义函数,以及一个简单的“运行时库”。
自定义函数
一门编程语言如果连函数和方法都没有的话,那也就不能算是一门语言了。而且,实践表明,一门面向对象语言中的所有特性都可以通过过程式的语言要素来实现:一个方法也只不过是以一个对象为额外参数的函数而已。因此,增加对函数的支持就是实现一门语言的核心所在。
其实,这个东东也是很简单的啦。跟以前一样,还是让我们来看一下 C 语言中的函数是怎么实现的吧:
|
|
gcc 生成的汇编代码是这个样子滴:
|
|
其中的函数调用现在应该很容易认了吧。剩下的就是简单的样板代码了:
在函数的开头,首先是将寄存器 %ebp
压入堆栈,然后拷贝寄存器 %esp
到 %ebp
。而在函数的最后, leave
指令就是前面两条指令的逆操作,而 ret
指令则是从堆栈中弹出要返回到的指令地址(也就是调用该函数的那条指令的下一条指令)并跳转。为什么在这里要将 %esp
(也就是堆栈指针)拷到 %ebp
呢?嘛,一个很明显的好处就是你可以尽情的申请堆栈空间,然后在完事时简单地将 %ebp
拷回给 %esp
就行了。从上面就可以看到, GCC 已经充分利用了这一点,直接用 leave
指令来处理调用函数时对参数所申请的空间 – 反正手工释放也只是浪费时间而已。
这么说来的话,要做的事情应该就很简单了啊。
首先需要修改方法 Compiler#initialize
,创建一个用来保存所有函数定义的哈希:
|
|
然后增加一个输出所有函数定义的方法:
|
|
可以看到,这里也同时包括了 .globl
与 .type
与 .size
之类的东西。 .globl
的意思就是你想让这个函数也能够从其他文件(也就是编译单元)中调用,这在链接多个目标文件的时候是很重要的。我想 .type
和 .size
主要是用在调试的时候,分别用来表示一个符号对应的是一个函数,以及这个函数的大小。
除了这些之外,这个方法就很简单啦 – 它会通过调用 #compile_exp
方法来完成实际的工作。
我们再来增加一个用来定义函数的辅助方法:
|
|
然后在方法 #compile_exp
中增加如下的几行代码:
|
|
之所以要增加第一行代码,一方面是出于健壮性的考虑,同时这也允许我们用 nil
和空数组来表示“啥也不做”的意思,当你要定义一个空函数的时候就会用到这一点了。这样一来,第二行代码就不需要去检查将要定义的是不是一个空函数了。
不知道你注意到了没有,我们其实已经实现了对函数的递归定义。像 [:defun,:foo,[:defun, :bar, []]]
这样的代码完全是合法的。同时你也许会注意到,这个实现会导致两个函数其实都是可以从别处调用的。好吧,现在是没关系的啦,我们以后会处理这个的(要么不允许编写这样的代码,要么就只允许外层函数来调用内层函数 – 我还没有决定到底要做哪个啦)。
剩下的事情就是输出这些函数的定义了,因此我们在方法 #compile
中对 #output_constants
的调用之前增加如下的一行:
|
|
增加对一个运行时库的支持
首先,让我们将现在的 #compile
方法重命名为 #compile_main
,然后重新定义 #compile
方法如下:
|
|
之后是对常量 DO_BEFORE
和 DO_AFTER
的定义(如果愿意的话,你也可以把它们放在一个单独的文件中,我现在就直接把它们放在开头好了):
|
|
你得承认,你想看到的应该是更加高级一些的东东,但那样就违背我们最初的目标了。上面的代码对于实现一个运行时库来说已经足够了。当然,你也可以用一些只能通过 C 或者汇编才能实现的东西,只要把包含那些函数实现的目标文件给链接进来就可以了,因为我们一直都是在按照 C 语言的调用规则来办事的嘛。
让我们来测试一下吧。在 Compiler.new.compile(prog)
的前面加入下面的代码:
|
|
然后编译运行:
|
|
你可以在这里找到今天的成果。
对函数参数的访问吗?
今天还遗留了一个任务:实现对函数参数的访问。这个的工作量可是不小的。放心,我不会忘了这个的,这将会是第八篇文章的主题。我也不会让你等太久的啦,这次一定 :-)