用Scheme写一个Scheme编译器(一)
在博主的大学生涯中,感觉最头痛的一门课程就是编译原理了,学习完这门课程之后,虽然知道了LL,LR算法,和一系列与编译原理相关的术语,可是对它的了解一直停留在做题上,虽然博主一直希望能够通过自己写一个编译器来加深对编译原理的理解,可是用C语言写编译器真的是一场噩梦,每天大把的时间都花在了调试bug上,更没有时间和精力去思考有关编译原理的东西@~@。
编译的理论不好理解,但是使用C语言又很难将注意力集中于编译的学习之上,想学关于编译的知识还真是难啊。
后来在接触到函数式编程之后,特别是在学习过Scheme语言之后,博主又重新拾起了当初的那个幼稚的想法,用Scheme写一个编译器。
我先来介绍一下为什么要使用Scheme来写一个编译器。
1. 由于Scheme的语法是使用了S-expression的形式,所以Scheme的程序在语法分析的阶段中就有很明显的优势了(说白了就是我们可以跳过在传统语言中令人头痛的语法分析了:-P)
2. 在传统的编译器编写方式中,我们一上来就要定义一大堆程序的语法,然后在写语法分析算法的时候,要一次性把所有的语法都考虑到(这种不人道的行为简直会把人们的耐心给磨没了),而在我们这个编译器当中,我们可以一点一点地扩充语法,而不是一次性地就把编译器写好,这也是拜Scheme语言的灵活性所赐。
3. 优点还有很多,一时想不起来了^_^
博主将会把完成这个编译器以一系列的文章呈现出来,如果有任何错误,意见都可以直接和我进行联系(邮箱:875244595@qq.com)
我们今天就从数字开始,这个很简单,主要是希望大家熟悉一下Scheme的语法。(博主使用的是Ubuntu 14.04 操作系统,Scheme编译器使用的是DrRacket,C编译器使用的是gcc)
我们首先要准备两样东西,一个是编译出来的程序的运行时程序,一个是我们用Scheme编写的编译器程序
1 C运行时程序
这个运行时程序现在还是非常简单,仅需要一个printf函数,把编译出来的程序显示出来:
#include<stdio.h> int scheme_entry(); int main(int argc, char** argv) { printf("%d\n", scheme_entry()); return 0; }
2 Scheme编译程序
(define (compile-program x) (emit " .text") (emit " .global _scheme_entry") (emit " .def _scheme_entry; .scl 2; .type 32; .endef") (emit "_scheme_entry:") (emit "LFB0:") (emit " .cfi_startproc") (emit " pushl %ebp") (emit " .cfi_def_cfa_offset 8") (emit " .cfi_offset 5, -8") (emit " movl %esp, %ebp") (emit " .cfi_def_cfa_register 5") (emit " movl $~a, %eax" x) (emit " popl %ebp") (emit " .cfi_restore 5") (emit " .cfi_def_cfa 4, 4") (emit " ret") (emit " .cfi_endproc") (emit "LFE0:"))
这是编译器的主程序,emit函数就是把字符串输出到我们规定的output-port里面,这个程序的字符串由汇编构成,不熟悉汇编语言的朋友看起来会感觉比较奇怪,其中大部分我们都不需要了解,我们需要注意的是(emit " movl $~a, %eax" x),x就是我们的立即数,这里把它放入eax寄存器中,因为计算机在执行完一个函数后会把结果放入eax寄存器中。
(define (emit . args) (apply fprintf (compile-port) args) (newline (compile-port)))
这是emit函数的定义,将结果显示入我们规定的端口。
(define compile-port (make-parameter (current-output-port) (lambda (p) (unless (output-port? p) (error 'compile-port (format "not an output port ~s" p))) p)))
current-output-port在rnrs/io/ports-6模块中定义,我们需要(require rnrs/io/ports-6)。
(define (compile expr) (run-compile expr) (build) (execute))
这个函数帮我们执行函数的编译,链接,执行。
(define (run-compile expr) (let ((p (open-output-file "program.s" #:exists 'replace))) (parameterize ((compile-port p)) (compile-program expr)) (close-output-port p)))
我们将输出端口规定为program.s。
(define (build) (unless (not (false? (system (format "gcc -m32 -Wall -o program ~a program.s" (runtime-file) ))) ) (error 'make "could not build target")))
连接我们使用gcc与我们前面准备的C运行时程序连接。
(define (execute) (unless (not (false? (system "./stst > stst.out"))) (error 'make "produced program exited abnormally")))
执行程序。
好了,执行完以上步骤后,你就得到了一个能编译数字的编译器了,虽然很简单,但是这个编译器是我们以后构造更复杂的编译器的基础,希望感兴趣的朋友认真对待它。
最后,我们先休息一下,希望大家玩的开心