“关于编写编程语言的赤裸裸的真相”(2014)

2020-05-16 12:12:59

我的职业生涯一直是关于设计编程语言和为它们编写编译器。这对我来说是一种巨大的快乐和满足感,如果您决定设计和实现一种专业的编程语言,也许我可以帮助其他人观察一下您的需求。当然,这是一个全书篇幅的主题,所以我在这里只会顺其自然地谈几个要点,而避开在其他地方已经很好地讨论过的话题。

首先,你有很多工作要做。多年的工作,其中大部分将在沙漠中游荡。成功的机会对你不利。如果你没有强烈的自我激励去做这件事,它是不会发生的。如果你需要别人的认可和鼓励,这是不会发生的。

幸运的是,做这样一个项目不是一大笔钱的投资,即使你失败了,它也不会让你崩溃。即使你真的失败了,取决于项目进展到什么程度,它在你的简历上看起来也会相当不错,对你的职业生涯也有好处。

有一件事非常清楚,那就是语法很重要。这非常重要。这就像汽车上的造型-如果造型不吸引人,那么表演再火爆也无关紧要。语法必须是您的目标受众喜欢的内容。

尝试使用他们以前从未见过的东西,将使语言采用变得更难推销。

我喜欢把熟悉的句法和美感结合在一起。它必须在屏幕上看起来很好看。毕竟,您将花费大量时间来研究它。如果它看起来笨拙、笨拙或难看,那么它就会玷污语言。

也许令人惊讶的是,我建议的几件事不应该是考虑因素。这些都是虚假的神:

最大限度地减少击键。当程序员使用纸带时,这可能很重要,而且对于bash或awk这样的小型语言也很重要。对于较大的应用程序,编程时间花在读写上的时间要比写多得多,因此减少击键本身不应该是一个目标。当然,我并不是说大量的样板文件是个好主意。

易于解析。编写具有任意先行功能的解析器并不难。不应该为了在解析器中节省几行代码而影响语言的外观。请记住,您将花费大量时间盯着代码。这是第一位的。如下所述,它仍然应该是上下文无关文法。

最大限度地减少关键字的数量。这个指标很愚蠢,但我看到它反复出现。英语有一百万个单词,我不认为有任何迫在眉睫的短缺。只要用你正确的判断力就行了。

上下文无关文法。这实际上意味着代码应该是可解析的,而不必在符号表中查找。众所周知,C++不是上下文无关的语法。上下文无关的语法,除了使事情变得简单之外,还意味着IDE可以在不集成到大多数编译器前端的情况下进行语法突出显示,也就是说,第三方工具变得更有可能存在。

冗余。是的,语法应该是多余的。你们都听人说过,语句终止是不必要的,因为编译器可以解决这个问题。这是真的-但这种非冗余会导致无法理解的错误消息。考虑一种没有冗余的语法。然后,任何随机的字符序列都将是有效的程序。甚至不可能出现错误消息。良好的语法需要冗余,以便诊断错误并提供正确的错误消息。

久经考验,千真万确。如果没有非常有力的理由,对于熟悉的结构,最好坚持使用久经考验的真实语法形式。它确实缩短了该语言的学习曲线,并将提高采用率。想想看,如果这种语言交换了+和*的运算符优先级,人们会多么讨厌它。将分歧3保存为以前未见的特征,这也向用户发出这是新的信号。

一如既往,这些原则不应被视为口头禅。要有正确的判断力。任何盲目遵循的语言设计原则都会导致灾难。这些原则很少是正交的,而且经常发生冲突。这很像设计一座房子--把主衣橱做得更大,意味着主卧室会变小。这一切都是为了找到合适的平衡。

抛开语法不谈,语言的核心将是语义处理,也就是将意义赋予句法结构。这是您将花费大量设计和实现的地方。这很像你体内的器官--它们是看不见的,除非它们出了问题,否则我们不会去想它们。语义工作不会有太多荣耀,但它将是语言的全部意义所在。

一旦通过语义阶段,编译器就会进行优化,然后生成代码,统称为后端。这两个关都很有挑战性,也很复杂。我,我喜欢和这些东西打交道,抱怨说我不得不把时间花在其他问题上。但是,除非您真的喜欢它,并且需要一个相当精神错乱的程序员才能享受这类事情的奥秘,否则我建议您采用常识方法,使用现有的后端,如jvm、clr、GCC或llvm。(当然,我可以随时为您设置辉煌的数字火星后端!)

怎样才能最好地实施呢?我希望我至少能把你引向正确的方向。开始编写编译器的人经常使用的第一个工具是regex。正则表达式不是用于词法分析和解析的错误工具。罗伯·派克(Rob Pike)相当好地解释了原因。我将用杰米·扎温斯基(Jamie Zawinski)的名言来结束这一点:

有些人在遇到问题时会认为“我知道,我会使用正则表达式。”现在他们有两个问题。

更具争议性的是,我不会费心将时间浪费在词法分析器或解析器生成器和其他所谓的编译器上。他们是在浪费时间。编写词法分析器和解析器只占编写编译器工作的一小部分。使用生成器将占用与手工编写相同的时间,并且它会将您与生成器结合起来(这在将编译器移植到新平台时很重要)。生成器也有一个不幸的名声,那就是发出糟糕的错误消息。

正如我所提到的,错误消息是该语言实现质量的一个重要因素。毕竟,这是用户看到的。如果你想发布“语法错误”之类的错误信息,也许你应该考虑做一名特许会计师。好的错误消息出人意料地难以编写,而且通常直到您查看技术支持电子邮件,您才会发现错误消息有多糟糕。

打印第一条消息并退出。这当然是最简单的方法,而且效果出奇地好。大多数编译器的后续消息都非常糟糕,以至于实际的程序员除了第一条消息之外都忽略了其他消息。圣杯是在一次编译过程中找到所有实际错误,从而导致:

猜测程序员的意图,修复语法树,然后继续。这是一种非常流行的方法。我不知疲倦地尝试了几十年,结果却惨遭失败。编译器似乎总是猜测错误,随后使用“固定”语法树的消息错误得离谱。

下毒的方法。这非常类似于处理浮点NAN的方式。任何使用NaN操作对象的操作都会以静默方式产生NaN。将此应用于错误恢复,任何具有发生错误的叶的构造本身都被认为是错误的(但不会为其发出额外的错误消息)。因此,只要错误在代码段之间没有依赖关系,编译器就能够检测到多个错误。这是我们在D编译器中一直使用的方法,并且对结果非常满意。

在编译器的隐藏部分,用户还关心什么?速度。我听了一遍又一遍,过度编译的速度很重要。事实上,当我问一家公司选择D的天平时,编译速度通常是我听到的第一个问题。事实是,大多数编译器都是猪。要让人们对您的语言刮目相看,请向他们展示它的编译速度与按下编译命令上的Return键一样快。

想知道让编译器变得快速的秘诀吗?我是说尖叫,令人眼花缭乱,闪电般的速度?用SASE寄给我$,我会告诉你!好的,好的,我会把赤裸裸的贪婪放在一边,让你加入。

听起来太简单了,对吧?甚至是陈词滥调。但是如果你经常使用侧写器,请举手。老实说,每个人都说他们知道,但是分析器手册仍然保留着原始的收缩包装。我只是对那些从不使用分析器的程序员感到惊讶。但这对我来说很棒,因为它是一种永远不会停止支付红利的竞争优势。

瓦尔格林德。我怀疑valgrind几乎是单枪匹马地将C和C++从遗忘中拯救出来。我对这个工具赞不绝口。它为我这个容易出错的混蛋节省了数不清的令人沮丧的小时。

Git和GitHub。具有变革性的工具不多,但这些都是变革性的。它们不仅提供自动备份,而且使世界各地的人们能够在项目上进行协作。它们还提供了每行代码来自何处、来自谁的完整历史记录,以防出现法律问题。

自动化测试框架。编译器是极其复杂的野兽。如果没有不断的修订测试,项目将达到无法前进的地步,因为将添加更多的错误而不是改进。在此基础上添加一个覆盖率分析器,它将显示测试套件是否正在执行所有代码。

自动文档生成器。D项目当然构建了我们自己的(Ddoc),它也是变革性的。在Ddoc之前,文档只与代码有随机关联,而且它们往往彼此没有关系。在Ddoc之后,这两个被同步了。

布吉拉。这是一个自动错误跟踪工具。与我可怜的旧电子邮件和文件夹方案相比,这是一个巨大的飞跃,这个系统根本无法扩展。与过去相比,程序员对有缺陷的编译器的容忍度大大降低,这一点必须正面积极解决。

事后看来,有一种语义技巧是显而易见的,但安德烈·亚历山大雷斯库(Andrei Alexandresu)向我指出了这一点,那就是“降低”。在内部,它包括用更简单的语义结构重写更复杂的语义结构。例如,可以根据for循环重写While循环和foreach循环。然后,其余的代码只需处理for循环。事实证明,这在While循环的实现方式中发现了几个潜在的错误,因此这是一个很好的胜利。它还被用来重写Try-Finally语句等方面的作用域保护语句。在语义处理中可以找到的任何情况都将是实现的赢家。

如果发现该语言中有一些特殊情况规则阻止了这种重写,那么回去重新审视一下语言设计可能是个好主意。

任何时候,您都可以在语义构造的处理中找到共性,这是减少实现工作量和错误的机会。

很少有人提到,但至关重要的是,您将需要编写一个运行时库。这是一项重大工程。它将作为语言功能如何工作的演示,所以最好是好的。一些需要正确处理的关键问题:

I/O性能。大多数程序在I/O上花费大量时间。缓慢的I/O会使整个语言看起来很糟糕。基准是C STDIO。如果该语言有优雅、可爱的I/OAPI,但运行速度只有C I/O的一半,那么它就不会有吸引力。

内存分配。在大多数程序中,大部分时间都花在进行普通的内存分配上。如果弄错了,后果自负。

超越功能。好吧,我撒谎了。没有人关心超越函数的精确度,他们只关心它们的速度。我的证据来自于尝试将D运行时库移植到不同的平台,并发现底层的C超越函数经常无法通过D库测试套件中的准确性测试。C库函数在处理IEEE浮点数、无穷大、次法线、负0等方面也常常做得很差。我们通过自己实现超越函数来弥补这一点。超验浮点代码编写起来相当棘手且晦涩难懂,所以我建议您找到一个现有的库,您可以对其进行许可和改编。

人们在使用标准图书馆时经常会陷入一个陷阱,那就是用琐碎的东西填满它们。琐事是沙子堵塞了齿轮,只是必须永远带在身边的重担。我的一般规则是,如果解释函数的功能的行数比实现代码多,那么函数很可能是琐碎的,应该引导出去。

你已经做到了,你已经有了新语言的一个很棒的原型。这次又是什么?接下来是最难的部分。这是大多数新语言失败的地方。你将做每个新生的摇滚乐队都会做的事情--在商场、高中舞会、潜水酒吧等表演,慢慢地建立起观众群。对于语言,这意味着准备有关该语言的演示文稿、文章、教程和书籍。然后,去参加程序员会议,会议,公司,任何他们有你的地方,并炫耀它。你会习惯于公开演讲,甚至会发现你喜欢它(我非常喜欢它)。

有一件大事对你有利。随着互联网的全球覆盖,有一个可以立即接触到的全球观众。另一个有利的方面是,程序员会议、会议等都在寻找好的内容。他们喜欢谈论新的语言,新的编程理念等。我与观众的经验是他们很友好,会给你很多建设性的反馈。

当然,几乎可以肯定的是,您将被迫重新评估该语言的一些宝贵特性,并对其进行重新设计。