Kalyn:用于X86-64的自主托管Lisp编译器

2021-06-08 16:39:30

在我的春季2020年春季在Harvey Muddcollege的过程中,我从头开始开发了一个自主托管。本文遍历了该项目的许多兴趣。它被布局了,所以你可以读起来结束,但如果你对一个特定话题更感兴趣,请随时跳到那里。或者,看看Ongithub的项目。

Kalyn是一个自主主机编译器。这意味着编译器ISITELESEL以它知道如何编译的语言,而SOTHE编译器可以编译自己。自主托管编译器是常见的,是一个原因是在编译器上用于语言Xprobiby的程序员享受语言X的编写代码,所以倾向于以语言X的编译器倾斜。

Kalyn编译了一种我自己设计的编程语言,也叫了Kalyn。为新的编程语言开发自托管编译器的一个障碍是为了编制编译器的第一次,您必须有一个编译器:它是ached-and-eg的问题。解决此问题的最简单方法是TofiRST以不同的语言编写一个简单版本的编译器,然后使用该编译器来编译您的真实编译器。因此,Kalyn Compiler的aretwo实现:一个在Haskell和一个Inkalyn本身。首先,我使用Haskell实现来编译Akalyn实现,然后在此之后,我可以使用Kalynimplement进行编译。

我在Harvey Muddcollege的Compilers课程中启发了创造Kalyn。在这一课堂上,学生开发了一个简单的Swift的编程语言转录程序,为学期的过程开发了一个简单的Swift编程。但是,我留下了更多,原因很少:

大多数编译器已经设计和实施,只有几个部分作为作业。这可能是一个伟大的想法,形成了学习工作的比率,但我是从头开始做事的人们的态度。

我们在课堂上编制的语言并没有真正完全统一,以做任何认真的工作。此外,即使它是有效的软件工程是一个好主意,载体的编程风格和类似语言的编程风格并不真正“SparkJoy”。当我不在Theclock时,Iprefer以更具表现力的语言工作,如Haskell和Lisp。我在创建一个我实际上不想使用的编译器的语言时,我没有感到非常动力。

我们在课堂上工作的编译器并不是真正的“全堆栈”,因为它因为它重复使用了许多现有的软件组件。 forexample,我们使用了GNU链接器和汇编程序,以便我们以文本格式而不是BinaryFormat的X86-64汇编代码,并且我们利用了C标准库来避免实现内存管理和输入/输出原始.AGAIN,这是可能是一个良好的教育袭击的好主意,但我想从Sourcecode到装配Opcodes的整个垂直。

我从头开始创建了所有内容,包括链接器,TheastSmbler和标准库。结束upin可执行文件的每个字节都是由我的代码直接生成的。

我设计Kalyn尽可能地使其尽可能使用,以尽可能地编译。它具有很少的核心功能(forexample,没有列表,阵列,地图或类),这是真正的ageneral-progperation编程语言,因为这些功能可以在用户代码中掌握,而无需特殊编译器支持。用于针对自主主机编译器,我强迫自己提升先深度的可用性,因为我需要写一个整个编译器Inkalyn。

老实说,我认为Kalyn是一个很好的编程语言,我在它中享用代码。它类似于Haskell,但使用LISP语法,这是我只看到的东西。但是,因为我真的喜欢除了语法(我认为absoluteabomination),Kalyn在缺乏表达的语言上增加了一些东西,所以它感觉就像我创造价值。 (是的,显然Kalynwon不被用于任何实际项目,但对我来说很重要,但语言不能被描述为“基本上与X相同,但是也不工作”。)

它真的有效吗?是的! Kalyn可以编译自己。性能速度足够令人讨厌,但与Haskell相比,不足以成为一个足够的速度。以下是统计数据:

因此,我们可以看到Kalyn比Haskell慢得多大约25倍,因为鉴于哈斯克尔已经优化了几十年的专家,我基本上扔了可能有效的东西。

现在这是一个不同的数字视角,项目的大小是时间的函数。最终总量是23个模块的4,300行的Haskellcode,43个模块的3,400行Kalyn代码。 (为什么要更多的Kalyn?语法略有简明扼要,但主要是因为我必须实现整个Haskell StandardLibrary - 或者至少在编译器中使用的部分。你可以看到我绝对把一切都留给了最后一刻......

对于另一个关于开发过程的透视图,这里是添加和删除的累积总行数的图(因此,在任何给定时间的Projectize是线之间的垂直距离)。

Kalyn是Haskell和Lisp的组合。这是一些Haskell代码的一个例子,它可以打印出最多100个:

模块主要在哪里 - |检查号码是否是素数。 isprime :: int - > Bool Isprime Num =让因素= [2 .. num - 1]在所有(\ factor-> num`mod` factor / = 0)主管主页:: io()main =让nums = [2 .. 100] Primes = Print Primes中的筛选Isprime数

以下是Clojure中的代码,在JVM上运行的最近开发的Lisp。

(NS Hello-World.Core)(Defn Prime?"检查号码是否是素数。 (零?(mod n因子))))))))))(defn -main [](let [nums(范围2 100)primes(滤光片Prime?nums)](println primes))))

这里是等效的Kalyn代码,您可以将Haskell的TheIdea与Lisp的语法相结合:

(进口" stdlib.kalyn")(Defn Isprime(Func Int Bool)"检查号码是否是素数。 2( - num 2)))))(所有(lambda(因素)(/ = int 0(%num因子)))因子))))(公共def main(io空)(设(nums(iterate(+ 1 )2 98))(PRIMES(筛选isprime数)))(打印(附加(播放显示次摘要)" \ n")))))))))))))

语言实际上非常小,所以我们可以快速完成所有ITPRETTY。让我们来看看。

为什么只有一个大小的整数?这使得代码生成使得每个整数具有相同的大小。实际上,我设计了Kalynusing所谓的盒装内存表示,因此每个数据类型都具有相同的大小。更稍后的更多。

角色怎么样?这些实际上只是作为整数存储。这浪费了很多空间,因为64中的56位留下了未使用,但如果我们没有关于不同的数据类型的拖网,它再次使实现更简单。

Kalyn拥有一流的功能,这意味着代码可以像任何其他数据类型一样动态创建运行时和Passthem的功能。这是支持合理的函数alprogramments.kalyn的函数有封闭,这需要特殊的编译器支持。更稍后的更多信息。

Kalyn中的所有功能都是自动核心的,就像在Haskell中一样。这意味着所有函数只采用一个参数;多参数函数实现为单参数函数,返回返回其他函数的另一个参数函数,返回返回其他函数函数,依据return return and thatter return函数,等等。我做出了这个决定的两个原因:首先,因为咖喱是很棒的,其次是因为它造成了类型的系统和代码生成,如果函数所有曲目相同的参数。

由于函数是核发的,所以符号Func A B C对于Func A(Func B C)的简写,其中A,B和C是可能代表INT和LISTSTRING和FUNC String INT等内容的类型参数。

有一件事你可能想知道是如何没有参数的函数。答案是没有这样的东西。由于评估到一定功能没有副作用(请参阅Monadic IO上的下一节),因此没有返回显示器表达式的参数的函数与返回该表达式本身的函数之间没有区别。

Kalyn采用Haskell的Monads宿币的抽象。解释MONADS超出了本质的范围,但重点是,在文章库中的每个输入/输出函数(打印,readfile,writefile等)并不行为。相反,它返回IO Monad的实例,表示IO操作。然后,这些实例可以使用功能编程技术来勾选,并且才能仅当从Program的主要功能返回时异。

IO Monad的每个实例都有一个返回类型,如haskell,所以TheType表示为Io Int或IO(列表字符串)或Io A一般。

您可能会认为使用Monadic IO与制作Kalyn的DesignGoal尽可能轻松地进行冲突。你会因为。但它太酷了!

您可能已经注意到,缺席Kalyn,您可能已经注意到大多数有用的数据类型,例如布尔斯和列表。这是因为你可以轻松地发现自己。这是按照哈尔克尔的那样完成的,其中包含代数类型。以下是Kalyn标准库如何定义Haskell程序员将熟悉的一些HandyData类型:

(公共数据BOOL FALSE)(公共数据(也许a)没有(仅a))(公共数据(lr)(左l)(右r))(公共数据(对ab)(对ab))(公共数据数据(列表a)null(缺点a(列表a)))(公共别名word8 int)(公共数据char(char word8))(公共别名字符串(列表char))

通过支持对任意代数数据类型的支持,编译器不需要对Booleans,List,阵列,映射,对,选项或将复杂化的其他任何其他复杂的特殊支持。

Kalyn包括声明和表达式,两者都是与哈斯克尔除外的声明和表达式。

首先,我们有函数调用,这是列出的。功能Currying自动处理,以便(映射(+ 1)elts)表示我们将+函数与参数1称为+函数,然后将其传递给MapFunction,并采取从地图返回的函数并将其传递给它。

接下来,您可以使用lambda定义匿名函数,因此前一段代码的MoreeXplice形式将是:

类型检查器包括约束求解器,因此它可以自动配置匿名功能的类型;没有必要手动(而且为简单地提供)TOMETCEIFY。

lambdas可以有多个参数,但这只是意味着它们公正的核心,因此(lambda(x y)......)与(lambda(x)(lambda(y)...)))相同。

(设(nums(nums(iterate(+ 1)2 98))(primes(筛选isprime nums)))(打印(Showlist Sprint Primes)))

按顺序评估每个结合,并且它可以指不奏的绑定,也可以递归地。这允许您批量递归匿名功能:

Let绑定中不支持的相互递归,因为在具有多个绑定的内部将retform翻译成一系列嵌套绑定的符号,这使得代码生成更容易。

最后一个特殊形式是情况(如haskell)允许您根据代数数据类型连接不同的值。数据构造函数和变量的ArbitraryPatterns可以在每个分支的小兵侧使用。例如,这里是来自Haskell的Classic Unzip函数的Kalyn'Simplation:

(公共污染解压缩(Func(parent(对ab))(对(列表a)(list b)))(对)(病例对(null(对null null))((缺点(左右右)对)(设(((配对左权限)(解压缩)))(对(缺点左侧)(涉及权利权限))))))))

您可能会注意到让表单采用破坏性,这与在案例分支中使用的模式匹配相同。这也可以在函数参数中完成,并且@ syntaxfrom haskell允许您同时命名值:

这是Kalyn中的核心表达式类型。解析器将介绍一些备注的语法,作为宏。例如,if语句

(缺点(符号72)(缺点(char 101)(缺点(char 108)(缺点(char 108)(缺点(char 111))))))

Variadic和和和或表单转换为嵌套的硬件表单。最后,我们有来自Haskell的经典符号,它转化为>> =调用。现在,正如我的稍后,Kalyn没有类型的类型,这意味着那种又是>>> =状态等。=每个monad的函数。作为aresult,您必须指定您在宏的初合形的Monad。它看起来像这样:

(DO IO(带内容(READFILE" in.txt"))(让versefile" out.txt" reversed)(SetFileMode" Out。 TXT" 0O600))

以表单相当于Haskell的< - 运算符,而令表格与Haskell中的相同。假设其他表单对忽略返回值的虚拟实例(LastForm除,它确定整个DO宏的返回值)。 Theabove代码如下所示:

(>> = IO(ReadFile"在in.txt")(Lambda(内容)(:(反向(反向内容)))(>> = io(writefile&#34 ;; .txt"逆转)(lambda(_)(setfilemode" out.txt" 0o600))))))))))))))))))

通过实现许多熟悉的语言功能作为宏而不是usttute表达式,我能够大大简化编译器的实现,因为只有解析器需要了解这些特征。

您可能想知道为什么让我们没有作为宏实现,此后所有((foo bar))......)相当于((lambda(foo(foo)bar)。答案是,这将引入大量的ofomoverhead,因为可以让可以轻松地翻译成组装中的单独指令,而函数调用(尤其适当处理闭包)则更昂贵。

首先,我们有def,它允许您定义符号的值,给出其类型和可选的docstring,如:

我们有型别名。这是来自haskell的类型关键字。(remype关键字与数据基本相同,而Kalyndoes不关心差异,因此它没有分离校准类型。)因此,例如,可以使用字符串作为列出的Ashorthand:

当然,只有一个尺寸的整数,并且二进制和文本字符串之间存在nodistintepty,但使用类型是有助于使类型签名更容易理解。

Kalyn编译器和标准库分为许多不同的文件。一个文件由编译器指定为主模块,Andit可以导入别人,如:

现在,每个声明关键字(def,defn,data,alias)可以归功于公共文档,以指示声明可用于导入模块的其他代码。作为匿名,这解决了我与Haskell的一个大烦恼,即没有办法指定模块中的哪个函数应该是公开的,必须在文件顶部列出所有它们。

理想情况下,Kalyn还将有一种方法来隐藏或选择进口的细则,但为了简单,我们没有避免。合格的导入将是另一个有用的功能,但在他们的情况下,只需使用前缀名称来避免冲突,例如MapInsert与setinsert。

一个关键特征是即使是导入关键字也可以在公共上之前,以指示所有导入的符号应该弯曲。这允许STDLIB.Kalyn到公共导入ManySubModules,以便用户代码只需要导入stdlib.kalyn来忘记整个标准库。

Kalyn的模块系统真的很简单。搜索路径或项目根目录没有概念。 Kalyn模块只是悬挂的Kalyn源代码(即使是文件扩展名并不重要),而且导入只能通过与导入的模块的DirectoryComent颁发的文件名解析为文件名。这简化了这一自动化;像Python这样的语言强加了更强烈的约定onmodule布局,但我们不需要它来获得编译器工作。

您可能已经注意到了Haskell的一个关键特征的显着缺席,即类型刻录物。事实证明,它不需要他们获得编译器,即使它们真的很好。在Haskell,例如,您的百思嘉百姓(例如,如果它们在标准库中定义):

实例显示bool,其中显示假="假"显示True ="真"实例显示a =>展示(列出A)显示elts =" [" ++ intercalate"," (地图显示elts)++"]"显示[false,true] - " [false,true]"

在Kalyn,我们可以做同样的事情,我们只需为每种类型定义一个不同的功能:

(别名(show a)(func一个字符串))(defn showbool(show bool)(bool)(案例bool(false" false")(true"真实"))( Defn Showlist(Func(Show A)(显示(列出A)))(显示elts)(concat [" ["(intercalate""(地图显示elts))和#34;]"]))Showlist showbool [false,true]; " [虚假,真实]"

不是理想的,但它看起来像Haskell版本如果你是yousquint,而且实际上它并不是那么大的痛苦。 Moreannoying是什么,这种方法不适用于更高遗传的艺术般的Monad。 (试一试并查看!)因此如果您将其传递给ArbitraryMonad的展示列表的风格,则无法定义一段功能,如果您将其传递给它,并且gt; =无论是什么绑定运算符。卢比,我们只使用两条Monads(IO和州)在编译器中,Sothat并没有太大的交易。

回想起来,我对结果非常满意。扩展TypeChecker以支持TypeClasses将是非常复杂的,因此我认为我实施的Thimited版本是一个很好的妥协,可以让自己托管的编译器从地面上获得。

来自Haskell的其他主要区别,值得一提的是难题。默认情况下,Haskell非常懒惰,因此表达式在需要时唯一评估。这常常以评估顺序造成损坏,并使难以理解正在运行的时间,尽管它确实使一些能够掌握了人身人身统计列表。 Kalyn采取更简单的方法,并急切地评估。做ThingSthis的方式有两个主要缺点:

您不能再现无限列表,所以习语,如拍摄100(迭代(+ 1)0)不起作用。我在TheStandard库中取得了迭代函数,占据了一个额外的论点,可以控制误报的数字,所以我们可以编写(迭代(+ 1)0 100),而是很棒的Itworks。事实证明,懒惰实际上并不需要所有的opropten,至少在这种项目中。

通常情况下,懒惰评估的方式是,每个表达式都isturned inthunk,可以在需要时计算atvalue,然后缓存。通过展备任何此项,我们丢失了缓存。这意味着顶级的值

......