我的Lisp编译器

我非常高兴地宣布我的第一个编程语言编译器已完成! Malcc是用C编写的增量Lisp AOT编译器。

我将简要介绍它的多年发展以及我在此过程中学到的知识。 替代文章标题:“如何在十年或更短的时间内编写一个编译器”。

(最后是TL; DR ,如果您不关心背景的话)。

编译器演示


tim ~/pp/malcc master 0 → ./malcc Mal [malcc] user> (println "hello world") hello world nil user> (+ 1 2) 3 user> (def! fib2 (fn* (n) (let* (f (fn* (n1 n2 c) (if (= cn) n2 (f n2 (+ n1 n2) (+ c 1))))) (f 0 1 1)))) <lambda> user> (fib2 25) 75025 user> ^D% tim ~/pp/malcc master 0 → ./malcc examples/hello.mal hello world tim ~/pp/malcc master 0 → ./malcc --compile examples/hello.mal hello gcc -g -I ./tinycc -I . -o hello hello.c ./reader.c ./printer.c ./hashmap.c ./types.c ./util.c ./env.c ./core.c ./tinycc/libtcc.a -ledit -lgc -lpcre -ldl tim ~/pp/malcc master 0 → ./hello hello world tim ~/pp/malcc master 0 → 

成功失败


近十年来,我一直梦想着编写一个编译器。 我一直着迷于编程语言的工作,尤其是编译器。 尽管我把编译器想象成是黑魔法,并且理解像我这样的凡人不可能从头开始制作它。

但是我仍然在尝试和研究!

一,口译员


2011年,我开始为虚构语言Airball(airball可以翻译为“莫夫”)编写一个简单的口译员。 顾名思义,您可以评估我不确定该方法是否有效的程度。 这是一个相当简单的Ruby程序,它解析代码并遍历抽象语法树 (AST)。 当解释器仍然有效时,我将其重命名为Lydia并将其重写为C以使其更快。



我记得Lydia的语法对我来说似乎很聪明! 我仍然喜欢它的简单性。

尽管Lydia远非完美的编译器,但它启发了我继续进行实验。 但是,问题仍然困扰着我:如何使编译器正常工作: 编译成什么? 我需要学习汇编程序吗?

其次,字节码编译器和解释器


下一步,在2014年,我开始研究scheme-vm ,这是用Ruby编写的Scheme的虚拟机 。 我认为具有自己的堆栈和字节码的虚拟机将是具有AST传递和成熟的编译器的解释器的过渡阶段。 而且由于Scheme是正式定义的 ,因此无需发明任何东西。

三年多来,我一直在迷惑scheme-vm,并从中学到了很多有关编译的知识。 最后,我意识到我无法完成这个项目。 代码变成了真正的混乱,但是看不到尽头。 没有导师或经验,我似乎在黑暗中徘徊。 事实证明,语言规范与它的手册不同 。 经验教训!

到2017年底,我推迟了scheme-vm以寻找更好的东西。

与马尔会面




在2018年的某个时候,我遇到了Clojure风格的Lisp口译员Mal

Mal是乔尔·马丁(Joel Martin)发明的一种培训工具。 从那时起,已经开发了超过75种不同语言的实现! 当我研究这些实现时,我意识到它们确实有帮助:如果遇到问题,可以去Ruby或Python版本中寻找提示。 最后,至少有人说我的语言!

我还认为,如果我可以为Mal编写解释器,则可以重复相同的步骤-并为Mal创建编译器。

Rust的口译员


首先,我根据演练开始开发口译员。 当时,我一直在积极研究Rust(我将在另一篇文章中进行介绍),因此我在Rust中编写了自己的Mal实现: mal-rust 。 有关此实验的更多信息,请参见此处。

真是太高兴了! 我不知道如何感谢或赞扬乔尔(Joel)撰写的关于Mal的出色指南。 每个步骤都有详细说明 ,其中有流程图,伪代码和测试 ! 开发人员从头到尾创建一种编程语言所需的一切。

在本教程结束时,我设法在Rust实现的基础上运行了用Mal编写的Mal的Mal实现。 (两个层次的深度,哇)。 当她第一次工作时,我兴奋地跳上椅子!

编译器Mal C


一旦证明了恶意软件的可行性,我立即开始研究如何编写编译器。 编译为汇编程序? 我可以直接编译机器代码吗?

我看到了用Ruby编写的x86汇编器。 他很吸引我,但是想到与汇编程序一起工作使我停了下来。

有一次,我偶然发现了有关Hacker News的评论 ,该评论Tiny C编译器为“编译后端”。 似乎是个好主意!

TinyCC有一个测试文件,显示了如何使用libtcc从C程序编译C代码,这是“ hello world”的起点。

在几个月的免费晚上和周末,再次回到Mal的演练中,回想起我对C的知识,我能够编写Mal编译器。 真的很高兴。



如果您习惯于通过测试进行开发,请评估一组初步测试的可用性。 测试导致可行的实施。

关于此过程,我不会说太多,除非我重复:Mal手册是真正的宝藏。 在每一步中,我都确切地知道该怎么做!

难点


回顾一下,在编写Mal编译器时遇到了一些困难,我不得不对此进行修补:

  1. 宏必须即时进行编译,并准备在编译时执行。 这有点困惑。
  2. 有必要为编译器代码和已编译程序的最终代码提供一个“环境”(哈希树/关联数组/带有变量及其值的字典)。 这使您可以在编译时定义宏。
  3. 由于环境是在编译时可用的,因此最初的Malcc在编译期间捕获了未定义的错误(访问未定义的变量),并且在一些地方,这违反了测试套件的预期。 最后,为了通过测试,我关闭了此功能。 最好将其重新添加为编译器的附加标志,因为这样可以预先捕获很多错误。
  4. 我通过编写三行结构来编译C代码:
    • top :顶级代码-这是函数
    • decl :声明和初始化体内使用的变量
    • body :主要工作完成的身体
  5. 我整日都在想是否可以编写自己的垃圾收集器,但是我决定将本练习留给以后使用。 Boehm-Demers-Weiser垃圾收集库易于连接,可在许多平台上使用。
  6. 查看编译器编写的代码很重要。 每当编译器遇到DEBUG环境变量时,它都会返回可查看错误的已编译C代码。

否则我会怎么做


  1. 编写C代码并尝试保持缩进并不容易,因此我不会拒绝自动化。 在我看来,有些编译器编写难看的代码,然后在发布它之前,使用特殊的库“修饰”它。 需要研究!
  2. 在代码生成过程中添加到行有点混乱。 您可以考虑创建一个AST,然后将其转换为C代码的最后一行,这应该使代码井井有条并保持和谐。

现在建议


我喜欢编译器花了将近十年的时间。 真的不行 道路上的每一步都令人愉快地回忆着我如何逐渐成为一名更好的程序员。

但这并不意味着我就“完成了”。 您仍然需要学习数百种方法和工具,才能真正成为一名编译器作者。 但是我可以自信地说:“我做到了。”

这是简洁的整个过程,说明如何制作自己的Lisp编译器:

  1. 选择您感到舒适的语言。 您不想同时学习一种新语言以及如何编写另一种新语言。
  2. 遵循Mal手册,编写翻译。
  3. 欢喜!
  4. 再次按照说明进行操作,但是不要执行代码,而是编写执行代码的代码。 (不仅是“重构”现有的解释器。您需要从头开始,尽管不禁止复制粘贴)。

我相信该方法可以与编译成可执行文件的任何编程语言一起使用。 例如,您可以:

  1. Go上编写Mal口译员。
  2. 修改您的代码以:
    • 创建一行Go代码并将其写入文件;
    • 使用go build编译此结果文件。

理想情况下,最好将Go编译器作为一个库进行控制,但这也是制作编译器的一种方式!

借助Mal的指南和您的独创性,您可以完成所有这些操作。 如果可以,那么您可以!

Source: https://habr.com/ru/post/zh-CN446808/


All Articles