作为个人观念中计算机科学中的三大浪漫之一,在下终于在这个学期有机会去实现了一个相对较为完整的编译器。虽然仅仅是作为课程作业,不过也实现了编译器所需要的许多部分:Parser, Optimizer, Instruction Selection Module等等,而除此之外也从个人的角度去了解了一些有关编译器或者编译器实现的信息,在此记录下来权当一份备忘。而这第一篇,我们先说说我们用来实现这个编译器的语言:Haskell。
现在应该不存在任何一个成熟语言不能被用来实现编译器了,从传统的选择如C, Caml,到相对较新的Go, Rust,几乎都有用其写成的编译器,甚至是编译本身的编译器,甚至是官方用来编译自己的编译器。而对于这个贯穿了整个学期的大作业,我们选择了Haskell作为实现语言。而从结果来看,这个选择给我们带来了许多便利,当然也有一些麻烦。
从便利的角度来讲,Haskell有着很方便的Data Constructor语法,与之配套的Pattern Match和Guard Expression,以及非常强大的<del>被囚禁了一万年的力量</del>Monad。前者可以帮助我们很好的去进行模式匹配,特别是在编写优化代码的时候,例如:
mergeStmts (IRMov toTemp@(IRTemp _ _) val)
(IRMov loc (IRLoc fromTemp@(IRTemp _ _)))
| toTemp == fromTemp = [IRMov loc val]
mergeStmts (IRAsgnOp toTemp@(IRTemp _ _) op val1 val2)
(IRMov loc (IRLoc fromTemp@(IRTemp _ _)))
| toTemp == fromTemp = [IRAsgnOp loc op val1 val2]
mergeStmts lastStmt newStmt = [lastStmt, newStmt]
当然这并不是Haskell所独有的,其他的函数式编程语言,例如OCaml也有相似的语法,不过如果在C或者Python里面,大概就要用宏或者Meta Programming来替代了。
而后者,作为笔者初学Haskell时记忆最深的东西,在笔者经过一个学期从觉得各种坑到离开Monad就不会写程序的转变之后,笔者的感觉是Monad确确实实很好用,一方面,它可以允许程序员在特定的时候使用更加贴近过程式语言的文法来减少行数,进而减少错误;另一方面,配合不同的Monad以及Haskell强大的类型系统,它可以有效地帮助避免编写时的错误,并且提高可读性,例如是分离有副作用和没有副作用的代码。当然Monad本身几句话是说不完的,一个比较有趣的文章是Monad Transformer and Modular Interpreter,介绍了常用的State,Reader,Writer等Monad在实现解释器中的作用。
而从麻烦的角度来说,最大的问题当然是
教练,我们都没用过Haskell啊!
其次则是关于惰性求值。惰性求值意味着当且仅当一个变量的值真的被用到(例如IO操作,Trace),它和它的子表达式才会被求值,所以写程序的时候务必要时刻记着这一点,特别是<del>你想用你的方式守护你心爱的程序的时候</del>使用一些Unsafe或者IO相关的东西的时候。例如我们为了降低编译器超时的可能性,选择对寄存器染色进行限时,但这是要务必用Seq或者DeepSeq保证实际的染色结果在时限内被完全求值,否则限时就只是在限制构造出这段程序本身了。
而最后则是性能问题。编译器中的特定部分,例如上文提到的寄存器染色复杂度很高,而Haskell中由于所有的变量都是不变量,绝大多数操作,包括Map,都需要至少O(log N)的复杂度,除非用ST Monad或者IO Monad相关的可变容器。笔者在这个问题上选择的更加激进的做法:用C++实现模块,在Haskell中通过FFI调用,但也遇到了各种Segmentation Fault什么的,而且,并没能完全解决掉。
而一点题外话则是Haskell本身的编译系统很值得了解,虽然笔者只是看了一些皮毛,但是还是感受到了<del>如此强大的力量(你要玩多少遍这个梗啊)</del>。通过巧妙地运用指针低位等黑科技,Haskell的编译器能够生成非常高效的的代码,如果没有被程序员误用的话。
(待续)