编译器如何编译自己?

时间:2016-06-18 20:11:01

标签: compilation

我正在http://coffeescript.org/网站上研究CoffeeScript,它有文字

  

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

编译器如何编译自己,或者这个语句是什么意思?

10 个答案:

答案 0 :(得分:215)

第一版编译器不能用特定于它的编程语言机器生成;你的困惑是可以理解的。可以由第一个编译器构建具有更多语言特性的编译器的更高版本(在新语言的第一版本中重写源)。然后该版本可以编译下一个编译器,依此类推。这是一个例子:

  1. 第一个CoffeeScript编译器是用Ruby编写的,它产生了CoffeeScript的第1版
  2. CS编译器的源代码在CoffeeScript 1
  3. 中重写
  4. 原始CS编译器将新代码(在CS 1中编写)编译为编译器的第2版
  5. 对编译器源代码进行了更改以添加新的语言功能
  6. 第二个CS编译器(第一个用CS编写的编译器)将修订后的新源代码编译成编译器的版本3
  7. 每次迭代重复步骤4和5
  8. 注意:我不确定CoffeeScript版本是如何编号的,这只是一个例子。

    此过程通常称为bootstrapping。引导编译器的另一个示例是rustcRust language的编译器。

答案 1 :(得分:57)

在论文Reflections on Trusting Trust中,Ken Thompson是Unix的创始人之一,他编写了一篇关于C编译器如何编译自身的精彩(易读)概述。类似的概念可以应用于CoffeeScript或任何其他语言。

编译器编译自己的代码的想法与quine:源代码非常类似,源代码在执行时会产生原始源代码作为输出。 CoffeeScript quine的Here is one example。汤普森举了一个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 本文中描述的情节扭曲是因为编译器可以被“教导”这样的事实,所以也可能被误导以难以生成的方式生成特洛伊木马可执行文件检测,这种破坏行为可以在被污染的编译器生成的所有编译器中持续存在。

答案 2 :(得分:30)

你已经得到了一个非常好的答案,但是我想为你提供一个不同的视角,希望对你有所帮助。让我们首先确定两个我们都同意的事实:

  1. CoffeeScript编译器是一个可以编译用CoffeeScript编写的程序的程序。
  2. CoffeeScript编译器是用CoffeeScript编写的程序。
  3. 我相信你能同意#1和#2都是真的。现在,看看这两个陈述。你现在看到CoffeeScript编译器能够编译CoffeeScript编译器是完全正常的吗?

    编译器并不关心它编译的。只要它是用CoffeeScript编写的程序,它就可以编译它。而CoffeeScript编译器本身恰好就是这样一个程序。 CoffeeScript编译器并不关心它正在编译的CoffeeScript编译器本身。它看到的只是一些CoffeeScript代码。周期。

      

    编译器如何编译自己,或者这个语句是什么意思?

    是的,这正是该陈述的含义,我希望你现在可以看到这种陈述是如何实现的。

答案 3 :(得分:9)

  

编译器如何编译自己,或者这个语句是什么意思?

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

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

现在,显而易见的是,您可以使用CoffeScript编译器生成的程序集(可执行文件)来编译任意CoffeScript程序,并为该程序生成程序集。

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

您的混淆似乎源于这样一个事实:当您创建自己的新语言时,您不会拥有编译器,但您可以使用它来编译您的编译器。这肯定看起来像鸡蛋问题,对吧?

介绍名为bootstrapping的流程。

  1. 用现有语言编写编译器(如果是CoffeScript,原始编译器是用Ruby编写的),可以编译新语言的子集
  2. 您编写的编译器可以用新语言本身编译新语言的子集。您只能使用上述步骤中编译器可以编译的语言功能。
  3. 使用步骤1中的编译器从第2步编译编译器。这将为您提供一个最初使用新语言子集编写的程序集,并且能够编译新语言的子集。 / LI>

    现在您需要添加新功能。假设您只实现了while - 循环,但也希望for - 循环。这不是问题,因为您可以以for - 循环的方式重写任何while - 循环。这意味着您只能在编译器的源代码中使用while - 循环,因为您手头的程序集只能编译它们。但是你可以在编译器中创建函数,这些函数可以用它来编译和编译for - 循环。然后使用已有的程序集,编译新的编译器版本。现在你有一个编译器的程序集,它也可以解析和编译for - 循环!您现在可以返回编译器的源文件,并将您不想要的任何while - 循环重写为for - 循环。

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

    whilefor显然只是示例,但这适用于您想要的任何新语言功能。然后你就是CoffeScript现在的情况:编译器会编译自己。

    那里有很多文献。 Reflections on Trusting Trust是一个经典,每个对该主题感兴趣的人都应至少阅读一次。

答案 4 :(得分:7)

一个小而重要的澄清

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

您将第一个文件应用于第二个文件,生成第三个文件,它能够执行与第一个文件相同的编译操作(如果第二个文件定义了第一个文件未实现的功能,则可能更多),因此可以替换第一个文件。首先,如果你愿意的话。

答案 5 :(得分:4)

  1. CoffeeScript编译器最初是用Ruby编写的。
  2. 然后在CoffeeScript中重写了CoffeeScript编译器。
  3. 由于已经存在Ruby版本的CoffeeScript编译器,因此它用于创建CoffeeScript编译器的CoffeeScript版本。

    enter image description here 这被称为self-hosting compiler

    它非常普遍,通常是因为作者希望使用自己的语言来维持语言的成长。

答案 6 :(得分:3)

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

当我们说“编写/实现一种语言”时,我们实际上意味着实现了该语言的编译器或解释器。有一些编程语言可以编写实现该语言的程序(是同一语言的编译器/解释器)。这些语言称为universal languages

为了能够理解这一点,请考虑一下金属车床。它是一种用于塑造金属的工具。只使用该工具,就可以通过创建其零件来创建另一个相同的工具。因此,该工具是通用机器。当然,第一个是使用其他手段(其他工具)创建的,可能质量较差。但是第一个用于构建具有更高精度的新的。

3D打印机几乎是一台通用机器。您可以使用3D打印机打印整个3D打印机(您无法构建熔化塑料的尖端)。

答案 7 :(得分:3)

通过归纳证明

诱导步骤

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

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

基本案例

但必须编译用X编写的第一个编译器版本 由X编译器编写的,用X以外的语言编写。这一步称为bootstrapping编译器。

答案 8 :(得分:0)

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

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

基本上编译是一种公正的翻译,而语言水平通常是较低级别的语言,但有很多变种。

引导编译器当然是最令人困惑的,因为它们编译了它们所编写的语言。不要忘记引导的初始步骤,这至少需要一个可执行的最小现有版本。许多引导编译器首先处理编程语言的最小特性,并且只要可以使用先前的特征表达新特征,就可以添加其他复杂的语言特性。如果不是这种情况,它将需要拥有"编译器的那部分"事先用另一种语言发展。

答案 9 :(得分:0)

虽然其他答案涵盖了所有要点,但我认为不包括可能是从其自己的源代码启动的编译器的最令人印象深刻的示例是很遗憾的。

几十年前,一个名叫道格·麦克罗伊(Doug McIlroy)的人想为一种称为TMG的新语言构建编译器。他用纸和笔用TMG语言本身写出了一个简单的TMG编译器的源代码。

现在,如果他只有TMG解释器,他就可以使用它来在自己的源代码上运行TMG编译器,然后他将拥有一个可运行的机器语言版本。但是...他没有已经有了TMG interpreter!这是一个缓慢的过程,但是由于输入量很小,因此足够快。

Doug在TMG解释器上的眼睛后面的那张纸上运行源代码,向其提供与输入文件完全相同的源代码。在编译器工作时,他可以看到从输入文件中读取了令牌,调用堆栈在进入和退出子过程时不断扩大和收缩,符号表不断增长……以及当编译器开始向其“输出”发出汇编语言语句时文件”,道格拿起他的笔,将它们写下来在另一张纸上。

编译器完成执行并成功退出后,Doug将生成的手写程序集清单带到计算机终端,键入它们,然后他的汇编器将其转换为工作的编译器二进制文件。

因此,这是“使用编译器进行自我编译”的另一种实用(???)方法:即使在“硬件”是湿软的且由花生酱三明治驱动的情况下,也可以在硬件中使用工作语言实现! / p>