[翻译] 用 Ruby 写编译器之四:自定义函数,以及运行时支持

原文链接:http://www.hokstad.com/writing-a-compiler-in-ruby-bottom-up-step-4.html


抱歉,又拖了很长时间。要忙的事情实在很多。正如上一篇文章末尾提到的那样,这次要讲的是自定义函数,以及一个简单的“运行时库”。

自定义函数

一门编程语言如果连函数和方法都没有的话,那也就不能算是一门语言了。而且,实践表明,一门面向对象语言中的所有特性都可以通过过程式的语言要素来实现:一个方法也只不过是以一个对象为额外参数的函数而已。因此,增加对函数的支持就是实现一门语言的核心所在。

其实,这个东东也是很简单的啦。跟以前一样,还是让我们来看一下 C 语言中的函数是怎么实现的吧:

1
2
3
4
void foo()
{
  puts("Hello world");
}

gcc 生成的汇编代码是这个样子滴:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.globl foo
        .type   foo, @function
foo:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp
        movl    $.LC0, (%esp)
        call    puts
        leave
        ret
        .size   foo, .-foo

其中的函数调用现在应该很容易认了吧。剩下的就是简单的样板代码了:

在函数的开头,首先是将寄存器 %ebp 压入堆栈,然后拷贝寄存器 %esp%ebp 。而在函数的最后, leave 指令就是前面两条指令的逆操作,而 ret 指令则是从堆栈中弹出要返回到的指令地址(也就是调用该函数的那条指令的下一条指令)并跳转。为什么在这里要将 %esp (也就是堆栈指针)拷到 %ebp 呢?嘛,一个很明显的好处就是你可以尽情的申请堆栈空间,然后在完事时简单地将 %ebp 拷回给 %esp 就行了。从上面就可以看到, GCC 已经充分利用了这一点,直接用 leave 指令来处理调用函数时对参数所申请的空间 – 反正手工释放也只是浪费时间而已。

这么说来的话,要做的事情应该就很简单了啊。

首先需要修改方法 Compiler#initialize ,创建一个用来保存所有函数定义的哈希:

1
2
  def initialize
    @global_functions = {}

然后增加一个输出所有函数定义的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
  def output_functions
    @global_functions.each do |name,data|
      puts ".globl #{name}"
      puts ".type   #{name}, @function"
      puts "#{name}:"
      puts "\tpushl   %ebp"
      puts "\tmovl    %esp, %ebp"
      compile_exp(data[1])
      puts "\tleave"
      puts "\tret"
      puts "\t.size   #{name}, .-#{name}"
      puts
    end
  end

可以看到,这里也同时包括了 .globl.type.size 之类的东西。 .globl 的意思就是你想让这个函数也能够从其他文件(也就是编译单元)中调用,这在链接多个目标文件的时候是很重要的。我想 .type.size 主要是用在调试的时候,分别用来表示一个符号对应的是一个函数,以及这个函数的大小。

除了这些之外,这个方法就很简单啦 – 它会通过调用 #compile_exp 方法来完成实际的工作。

我们再来增加一个用来定义函数的辅助方法:

1
2
3
  def defun name, args, body
    @global_functions[name] = [args, body]
  end

然后在方法 #compile_exp 中增加如下的几行代码:

1
2
    return if !exp || exp.size == 0
    return defun(*exp[1..-1]) if (exp[0] == :defun)

之所以要增加第一行代码,一方面是出于健壮性的考虑,同时这也允许我们用 nil 和空数组来表示“啥也不做”的意思,当你要定义一个空函数的时候就会用到这一点了。这样一来,第二行代码就不需要去检查将要定义的是不是一个空函数了。

不知道你注意到了没有,我们其实已经实现了对函数的递归定义。像 [:defun,:foo,[:defun, :bar, []]] 这样的代码完全是合法的。同时你也许会注意到,这个实现会导致两个函数其实都是可以从别处调用的。好吧,现在是没关系的啦,我们以后会处理这个的(要么不允许编写这样的代码,要么就只允许外层函数来调用内层函数 – 我还没有决定到底要做哪个啦)。

剩下的事情就是输出这些函数的定义了,因此我们在方法 #compile 中对 #output_constants 的调用之前增加如下的一行:

1
    output_functions

增加对一个运行时库的支持

首先,让我们将现在的 #compile 方法重命名为 #compile_main ,然后重新定义 #compile 方法如下:

1
2
3
  def compile(exp)
    compile_main([:do, DO_BEFORE, exp, DO_AFTER])
  end

之后是对常量 DO_BEFOREDO_AFTER 的定义(如果愿意的话,你也可以把它们放在一个单独的文件中,我现在就直接把它们放在开头好了):

1
2
3
4
DO_BEFORE= [:do,
  [:defun, :hello_world,[], [:puts, "Hello World"]]
]
DO_AFTER= []

你得承认,你想看到的应该是更加高级一些的东东,但那样就违背我们最初的目标了。上面的代码对于实现一个运行时库来说已经足够了。当然,你也可以用一些只能通过 C 或者汇编才能实现的东西,只要把包含那些函数实现的目标文件给链接进来就可以了,因为我们一直都是在按照 C 语言的调用规则来办事的嘛。

让我们来测试一下吧。在 Compiler.new.compile(prog) 的前面加入下面的代码:

1
prog = [:hello_world]

然后编译运行:

1
2
3
4
5
6
$ ruby step4.rb >step4.s
$ make step4
cc    step4.s   -o step4
$ ./step4
Hello World
$

你可以在这里找到今天的成果。

对函数参数的访问吗?

今天还遗留了一个任务:实现对函数参数的访问。这个的工作量可是不小的。放心,我不会忘了这个的,这将会是第八篇文章的主题。我也不会让你等太久的啦,这次一定 :-)

 
comments powered by Disqus