编译器如何进行编译?


168

我正在http://coffeescript.org/网站上研究CoffeeScript ,其中包含以下文字:

CoffeeScript编译器本身是用CoffeeScript编写的

编译器如何自行编译,或者该语句是什么意思?


14
可以自行编译的编译器的另一个术语是self-hosting编译器。见programmers.stackexchange.com/q/263651/6221
oɔɯǝɹ

37
为何编译器不能自行编译?
user253751 '16

48
至少有两个编译器副本。预先存在的一个会编译一个新副本。新的可能与旧的完全相同或不同。
bdsl

12
您可能还对Git感兴趣:当然,在Git存储库中跟踪了其源代码。
Greg d'Eon

7
这就像询问“ Xerox打印机如何自行打印原理图?”一样。编译器将文本编译为字节码。如果编译器可以编译为任何可用的字节码,则可以用相应的语言编写编译器代码,然后将代码传递给编译器以生成输出。
RLH

Answers:


219

编译器的第一版不能通过专用于它的编程语言来机器生成。您的困惑是可以理解的。可以由第一个编译器构建具有更多语言功能的更高版本的编译器(源代码用新语言的第一个版本重写)。然后,该版本可以编译下一个编译器,依此类推。这是一个例子:

  1. 第一个CoffeeScript编译器是用Ruby编写的,产生了CoffeeScript的版本1
  2. CS编译器的源代码在CoffeeScript 1中进行了重写
  3. 原始的CS编译器将新代码(用CS 1编写)编译到编译器的版本2中
  4. 更改了编译器源代码,以添加新的语言功能
  5. 第二个CS编译器(第一个用CS编写)将修改后的新源代码编译到编译器的版本3中
  6. 对每个迭代重复步骤4和5

注意:我不确定CoffeeScript版本的确切编号方式,这只是一个示例。

此过程通常称为自举。自举编译器的另一个示例是Rust语言rustc的编译器。


5
自举编译器的另一种方法是为您的语言(子集)编写解释器。
阿隆

作为用另一种语言编写的编译器或解释器进行引导的另一种选择,非常古老的方法是手工汇编编译器源代码。Chuck Moore在第9章“引导程序”中介绍了如何为Forth解释器执行此操作,方法是对面向问题的语言进行编程web.archive.org/web/20160327044521/www.colorforth.com/POL .htm),基于之前已手动完成两次。此处的代码输入是通过前面板完成的,该面板允许将值直接存储到由位拨动开关控制的存储地址中。
Jeremy W. Sherman'5

59

在Unix的发起者之一Ken Thompson的《Reflection on Trusting Trust》一书中,写了一篇关于C编译器如何进行自我编译的迷人(且易于阅读)的概述。类似的概念可以应用于CoffeeScript或任何其他语言。

编译自己的代码的编译器的思想与quine相似:源代码在执行时会产生原始源代码作为输出。 这是 CoffeeScript quine的一个示例。汤普森举了一个C quine的例子:

char s[] = {
    '\t',
    '0',
    '\n',
    '}',
    ';',
    '\n',
    '\n',
    '/',
    '*',
    '\n',
    … 213 lines omitted …
    0
};

/*
 * The string s is a representation of the body
 * of this program from '0'
 * to the end.
 */

main()
{
    int i;

    printf("char\ts[] = {\n");
    for(i = 0; s[i]; i++)
        printf("\t%d,\n", s[i]);
    printf("%s", s);
}

接下来,您可能想知道如何告诉编译器如何将转义序列'\n'表示为ASCII代码10。答案是,在C编译器中的某个地方,存在一个例程来解释字符文字,该例程包含一些可识别反斜杠序列的条件:

…
c = next();
if (c != '\\') return c;        /* A normal character */
c = next();
if (c == '\\') return '\\';     /* Two backslashes in the code means one backslash */
if (c == 'r')  return '\r';     /* '\r' is a carriage return */
…

因此,我们可以在上面的代码中添加一个条件...

if (c == 'n')  return 10;       /* '\n' is a newline */

…生成知道'\n'表示ASCII 10的编译器。有趣的是,该编译器以及由其编译的所有后续编译器 “知道”该映射,因此在下一代源代码中,您可以将最后一行更改为

if (c == 'n')  return '\n';

……它会做正确的事!将10来自编译器,而不再需要在编译器的源代码被明确定义。1个

这是用C代码实现的C语言功能的一个示例。现在,对每种语言功能都重复该过程,您将拥有一个“自托管”编译器:一个用C编写的C编译器。


1本文描述的情节转折之处在于,由于编译器可能会被“教”为事实,因此以难以检测的方式生成特洛伊木马可执行文件也可能会被误导,并且这种破坏行为会持续存在在受污染的编译器产生的所有编译器中。


7
尽管这是一些有趣的信息,但我认为它不能回答问题。您的示例假定您已经具有自举式编译器,或者C编译器使用哪种语言编写?
Arturo TorresSánchez16年

9
@ArturoTorresSánchez不同的解释适用于不同的人。我的目的不是要重申其他答案中所说的内容。相反,我发现其他答案的表达水平比我的想法更高。我个人更喜欢具体说明如何添加一个功能,并让读者从中进行推断,而不是简要介绍。
200_success,2016年

5
好,我了解您的观点。只是这个问题更多是“如果不存在要编译该编译器的编译器,那么该编译器如何进行自身编译”,而不是“如何向自举式编译器添加新功能”。
Arturo TorresSánchez16年

17
这个问题本身是模棱两可和开放性的。似乎有人将其解释为“ CoffeeScript编译器如何进行编译?”。注释中给出的轻率响应是:“为什么它不能像编译任何代码一样能够自我编译?” 我将其解释为“自托管的编译器如何出现?”,并举例说明了如何向编译器介绍其自身的语言功能。通过提供有关其实现方式的简要说明,它以不同的方式回答了该问题。
200_success

1
@ArturoTorresSánchez:“ [I] C编译器使用哪种语言编写?” 很久以前,我维护了旧的K&R附录(IBM 360的那个附录)中提到的原始C编译器。许多人知道,首先是BCPL,然后是B,C是B的改进版本。实际上,有很多该旧编译器的某些部分仍用B编写,并且从未被重写为C。这些变量采用单字母/数字形式,并且不假定指针算术是自动缩放的,依此类推。从B到C的第一个“C”编译器写于B.自举
埃利亚Skoczylas

29

您已经获得了很好的答案,但是我想为您提供不同的观点,希望会对您有所启发。首先让我们建立两个我们都可以同意的事实:

  1. CoffeeScript编译器是可以编译以CoffeeScript编写的程序的程序。
  2. CoffeeScript编译器是用CoffeeScript编写的程序。

我相信您可以同意#1和#2都是正确的。现在,看看这两个语句。您现在看到CoffeeScript编译器能够编译CoffeeScript编译器是完全正常的吗?

编译器不在乎它编译什么。只要它是用CoffeeScript编写的程序,就可以对其进行编译。CoffeeScript编译器本身恰好就是这样的程序。CoffeeScript编译器并不关心它是正在编译的CoffeeScript编译器本身。它所看到的只是一些CoffeeScript代码。期。

编译器如何自行编译,或者该语句是什么意思?

是的,这正是该声明的含义,我希望您现在可以看到该声明的正确性。


2
我对Coffee脚本了解不多,但是您可以通过声明它是用Coffee脚本编写的,但后来被编译为机器代码来澄清第二点。无论如何,请您再解释一下鸡肉和鸡蛋的问题。如果编译器是用尚未编写的语言编写的,那么该编译器如何运行或编译?
barlop

6
您的陈述2不完整/不准确,并且极具误导性。因为正如第一个答案所说,第一个不是用咖啡脚本写的。至于“编译器如何进行编译,或者该语句是什么意思?” 您说“是”,我想是这样(尽管我的想法有点小),但我看到它是用来编译自身而不是本身的早期版本的。但是它也可以用来编译自身吗?我认为这毫无意义。
barlop

2
@barlop:将语句2更改为“ 今天,CoffeeScript编译器是用CoffeeScript编写的程序”。这样有助于您更好地理解它吗?编译器只是将输入(代码)转换为输出(程序)的程序。因此,如果您具有用于Foo语言的编译器,然后使用Foo本身的语言编写Foo编译器的源代码,并将该源提供给您的第一个Foo编译器,则将获得第二个Foo编译器作为输出。这是由许多种语言完成的(例如,我所知道的所有C编译器都是用C编写的)。
DarkDust

3
编译器无法自行编译。输出文件与生成输出文件的编译器实例不同。希望您现在可以看到该陈述是错误的。
pabrams

3
@pabrams为什么会这样呢?输出结果可能与用于产生该结果的编译器相同。例如,如果我用GCC 6.1编译GCC 6.1,我会得到一个用GCC 6.1编译的GCC 6.1版本。然后,如果我用它来编译GCC 6.1,我还将得到一个用GCC 6.1编译的GCC 6.1版本,该版本应该是相同的(忽略诸如时间戳之类的东西)。
user253751 '16

9

编译器如何自行编译,或者该语句是什么意思?

就是这个意思。首先,要考虑一些事情。我们需要查看四个对象:

  • 任何任意CoffeScript程序的源代码
  • 任何任意CoffeScript程序的(生成的)程序集
  • CoffeScript编译器的源代码
  • CoffeScript编译器的(生成的)程序集

现在,很明显,您可以使用CoffeScript编译器的生成的程序集-可执行文件-编译任意CoffeScript程序,并为该程序生成程序集。

现在,CoffeScript编译器本身只是一个任意的CoffeScript程序,因此可以由CoffeScript编译器进行编译。

看来,你的困惑来源于这样的事实,当你创建自己的新的语言,你不要编译器还可以用来编译的编译器。这肯定看起来像个鸡蛋问题,对吧?

介绍称为引导程序的过程。

  1. 您使用现有语言编写了一个编译器(对于CoffeScript,原始编译器是用Ruby编写的),它可以编译新语言的一部分
  2. 您编写了一个编译器,可以使用新语言本身来编译新语言的子集。您只能使用上述步骤中编译器可以编译的语言功能。
  3. 您可以使用第1步中的编译器来编译第2步中的编译器。这将使您留下一个程序集,该程序集最初是用新语言的子集编写的,并且能够编译新语言的子集。

现在,您需要添加新功能。假设您仅实现了while-loops,但还需要for-loops。这不是问题,因为您可以用for-loop这样的方式重写任何while-loop。这意味着您只能while在编译器的源代码中使用-loops,因为您手头的程序集只能编译这些代码。但是您可以在编译器内部创建函数,从而可以for对其进行Pase 和编译-loops。然后,使用已有的程序集,并编译新的编译器版本。现在,您有了一个编译器的程序集,该程序集也可以解析和编译for-loops!现在,您可以返回到编译器的源文件,并将while不需要的任何-loops 重写为for-loops。

漂洗并重复直到可以使用编译器编译所需的所有语言功能。

whilefor显然只是例子,但这个工程的任何你想要的新的语言特性。然后您处于CoffeScript处于这种情况:编译器自行编译。

那里有很多文献。关于信任的思考信任是每个对该主题感兴趣的人的经典著作,至少应阅读一次。


5
(“ CoffeeScript编译器本身是用CoffeeScript编写的,这句话是对的,但是” A编译器可以自行编译”是错的。)
pabrams

4
不,完全正确。编译器可以自行编译。只是没有意义。假设您拥有可以编译该语言的版本X的可执行文件。您编写了一个可以编译X + 1版本的编译器,然后使用您拥有的编译器(X版本)对其进行编译。您最终获得了可以编译该语言的X + 1版本的可执行文件。现在您可以使用新的可执行文件重新编译该编译器。但是到底是什么?您已经具有执行所需功能的可执行文件。编译器可以编译任何有效程序,因此它可以完全编译自己!
Polygnome

1
确实确实有很多次编译是闻所未闻的,iirc现代的freepascal总共编译了5次。
plugwash

1
@pabrams编写“请勿触摸”和“热物体。请勿触摸”与该短语的预期消息没有区别。只要消息的预期读者(程序员)理解短语的预期消息(编译器的内部版本可以编译其源代码),无论其编写方式如何,此讨论都是没有意义的。到目前为止,您的论点无效。除非您能够证明消息的目标受众是非程序员,否则,只有这样,您才是正确的。
DarkDestry

2
@pabrams“良好的英语”是一种英语,可以按照作者或演讲者的意图清晰地将想法传达给目标受众。如果目标读者是程序员,并且程序员理解它,那它的英语很好。说“光既有粒子又有波同时存在”,基本上等同于“光既有光子又有电磁波同时存在”。对于物理学家来说,它们实际上是同一件事。这是否意味着我们应该始终使用更长更清晰的情感?没有!因为当目标受众已经清楚其含义时,它会使阅读变得很复杂。
DarkDestry '16

7

少量但重要的说明

在这里,术语“ 编译器”掩盖了涉及两个文件的事实。一个是可执行文件,它将以CoffeScript编写的输入文件作为输入,并生成另一个可执行文件,可链接目标文件或共享库作为其输出文件。另一个是CoffeeScript源文件,它恰好描述了编译CoffeeScript的过程。

您将第一个文件应用于第二个文件,生成第三个文件,该文件能够执行与第一个文件相同的编译操作(如果第二个文件定义了第一个文件未实现的功能,则可能会执行更多操作),因此如果您将第一个文件替换为第一个文件如此渴望。


4
  1. CoffeeScript编译器最初是用Ruby编写的。
  2. 然后用CoffeeScript重新编写CoffeeScript编译器。

由于CoffeeScript编译器的Ruby版本已经存在,因此可以使用它来创建CoffeeScript编译器的CoffeeScript版本。

在此处输入图片说明 这称为自托管编译器

这是非常普遍的,通常是由于作者希望使用自己的语言来保持该语言的发展所致。


3

这里不是编译器的问题,而是语言的表达性的问题,因为编译器只是用某种语言编写的程序。

当我们说“一种语言被编写/实现”时,实际上是指实现了该语言的编译器或解释器。有一些编程语言,您可以在其中编写实现该语言的程序(相同语言的编译器/解释器)。这些语言称为通用语言

为了能够理解这一点,请考虑一下金属车床。它是用于成型金属的工具。仅通过使用该工具,就可以通过创建其零件来创建另一个相同的工具。因此,该工具是通用机器。当然,第一个是使用其他方式(其他工具)创建的,并且质量可能较低。但是第一个用于构建更高精度的新产品。

3D打印机几乎是通用机器。您可以使用3D打印机来打印整个3D打印机(您不能构建熔化塑料的笔尖)。


我喜欢车床类比。但是,与车床类比不同,第一次编译器迭代中的缺陷会传递给所有后续编译器。例如,上面的答案提到添加一个for循环功能,其中原始编译器仅使用while循环。输出了解for循环,但实现是使用while循环的。如果原始的while循环实现有缺陷或效率低下,那么它将永远是!

@ Physics-Compute完全是错误的。在没有恶意的情况下,编译编译器时通常不会传播缺陷。
–plugwash

程序集翻译的确会在迭代之间传递,直到程序集翻译固定为止。以旧功能为基础的新功能不会更改基础实现。考虑一下。

@plugwash参见Ken Thompson撰写的“对信任信任的思考” -ece.cmu.edu/~ganger/712.fall02/papers/p761-thompson.pdf

3

归纳证明

归纳步骤

编译器的第n + 1版本用X编写。

因此,它可以由编译器的第n个版本(也用X编写)进行编译。

基本情况

但是用X编写的编译器的第一个版本必须由用X以外的语言编写的X编译器进行编译。此步骤称为引导编译器。


1
X语言的第一个编译器编译器可以很容易地用X编写。这可能是第一个编译器可以解释的。(由用X以外的语言编写的X解释器)。
卡兹

0

编译器采用高级规范,然后将其转换为低级实现,例如可以在硬件上执行的规范。因此,除了目标语言的语义外,规范的格式与实际执行之间没有任何关系。

交叉编译器从一个系统迁移到另一个系统,跨语言编译器将一种语言规范编译为另一种语言规范。

基本上,编译只是一种翻译,级别通常是高级语言到低级语言,但是有许多变体。

引导编译器当然是最令人困惑的,因为它们会编译其编写的语言。不要忘记引导的第一步,即至少需要一个最小的现有可执行版本。许多自举式编译器会首先使用编程语言的最小功能,然后再添加其他复杂的语言功能,只要新功能可以使用以前的功能表示即可。如果不是这种情况,则需要事先用另一种语言开发“编译器”的一部分。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.