用自己的语言编写编译器


204

凭直觉,似乎语言的编译器Foo本身不能用Foo编写。更具体地说,第一个语言编译器Foo不能用Foo编写,但是任何后续编译器都可以用编写Foo

但这是真的吗?我对阅读某种语言的记忆非常模糊,这种语言的第一个编译器是用“自身”编写的。这有可能吗?



这是一个非常老的问题,但是说我用Java为Foo语言编写了一个解释器。然后使用foo语言编写了自己的解释器。Foo仍然需要JRE吗?
乔治Xavier

Answers:


231

这称为“自举”。您必须首先使用其他某种语言(通常是Java或C)为您的语言构建编译器(或解释器)。完成后,您可以使用Foo语言编写新版本的编译器。您使用第一个引导程序编译器来编译该编译器,然后使用该编译器来编译其他所有内容(包括其自身的将来版本)。

实际上,大多数语言都是以这种方式创建的,部分原因是语言设计人员喜欢使用他们正在创建的语言,而且还因为非平凡的编译器通常可以作为该语言“完成”程度的有用基准。

一个示例就是Scala。它的第一个编译器是由Martin Odersky用实验语言Pizza创建的。从2.0版开始,编译器完全用Scala重写。从那时起,由于可以使用新的Scala编译器为将来的迭代进行编译,因此可以完全放弃旧的Pizza编译器。


可能是一个愚蠢的问题:如果要将编译器移植到微处理器的另一个体系结构,则引导程序应从该体系结构的有效编译器重新启动。这是正确的吗?如果这是正确的,这意味着最好保留第一个编译器,因为将您的编译器移植到其他体系结构可能会很有用(尤其是如果是用某种“通用语言”(例如C)编写的)?
piertoni '19

2
@piertoni通常,将编译器后端重新定位到新的微处理器通常会更容易。
bstpierre

使用LLVM作为后端,例如

76

我记得在听软件工程电台的播客时,迪克·加布里埃尔(Dick Gabriel)谈到了通过在纸上用LISP编写简写版本并将其手工组装成机器代码来引导原始LISP解释器的问题。从那时起,其余的LISP功能都被LISP编写并解释。


一切都是从创

47

好奇心增加了以前的答案。

这是《Linux From Scratch》手册的引言,这一步是从源头开始构建GCC编译器的步骤。(Linux From Scratch是一种安装Linux的方法,它与安装发行版完全不同,因为您实际上必须编译目标系统的每个二进制文件。)

make bootstrap

“ bootstrap”目标不仅可以编译GCC,还可以编译几次。它使用在第一轮中编译的程序进行第二次自身编译,然后再次进行第三次编译。然后,它将第二和第三次编译进行比较,以确保它可以完美地自我复制。这也意味着它已正确编译。

使用“ bootstrap”目标的动机是这样的事实,即编译器用来构建目标系统的工具链的版本可能与目标编译器的版本不完全相同。以这种方式进行操作,可以确保在目标系统中获得可以自行编译的编译器。


12
“实际上必须编译目标系统的每个二进制文件”,但是必须从某个地方获得的gcc二进制文件开始,因为源代码本身无法编译。我想知道您是否追溯了用于重新编译每个后续gcc的每个gcc二进制文件的谱系,是否会一直回到K&R的原始C编译器?
robru

43

当您为C编写第一个编译器时,会用其他某种语言编写它。现在,您可以在汇编器中使用C编译器。最终,您将到达必须解析字符串(特别是转义序列)的地方。您将编写代码以将\n其转换为带有十进制代码10的字符(以及转换\r为13 的字符,等等)。

该编译器准备好后,您将开始在C中重新实现它。此过程称为“ 引导 ”。

字符串解析代码将变为:

...
if (c == 92) { // backslash
    c = getc();
    if (c == 110) { // n
        return 10;
    } else if (c == 92) { // another backslash
        return 92;
    } else {
        ...
    }
}
...

编译时,您具有一个可以理解'\ n'的二进制文件。这意味着您可以更改源代码:

...
if (c == '\\') {
    c = getc();
    if (c == 'n') {
        return '\n';
    } else if (c == '\\') {
        return '\\';
    } else {
        ...
    }
}
...

那么“ \ n”是13的代码在哪里呢?在二进制文件中!就像DNA:使用此二进制文件编译C源代码将继承此信息。如果编译器自行编译,它将把这些知识传递给它的后代。从这一点开始,没有办法仅从源头上看编译器会做什么。

如果要在某个程序的源代码中隐藏病毒,则可以这样进行:获取编译器的源代码,找到可以编译函数的函数,然后用该函数替换它:

void compileFunction(char * name, char * filename, char * code) {
    if (strcmp("compileFunction", name) == 0 && strcmp("compile.c", filename) == 0) {
        code = A;
    } else if (strcmp("xxx", name) == 0 && strcmp("yyy.c", filename) == 0) {
        code = B;
    }

    ... code to compile the function body from the string in "code" ...
}

有趣的部分是A和B。A是compileFunction包括病毒的源代码,可能以某种方式进行了加密,因此从搜索所得的二进制文件中并不明显。这样可以确保使用编译器本身进行编译将保留病毒注入代码。

B与我们要用病毒替换的功能相同。例如,它可能是源文件“ login.c”中的“ login”功能,该功能可能来自Linux内核。我们可以将其替换为一个版本,该版本除了常规密码外还将接受root帐户的密码“ joshua”。

如果编译该文件并将其作为二进制文件进行传播,将无法通过查看源代码来查找病毒。

这个想法的原始来源:https : //web.archive.org/web/20070714062657/http : //www.acm.org/classics/sep95/


1
下半年关于编写受病毒感染的编译器有什么意义?:)
mhvelplund

3
@mhvelplund只是传播知识,如何引导会杀死您。
亚伦·迪古拉

19

您不能自己编写编译器,因为您没有什么可用来编译起始源代码。有两种解决方法。

以下是最不受欢迎的。您在汇编器(yuck)中编写了最小语言集的最小编译器,然后使用该编译器实现了该语言的其他功能。逐步构建,直到拥有自己具有所有语言功能的编译器为止。一个痛苦的过程通常仅在您别无选择时才执行。

首选方法是使用交叉编译器。您可以在另一台计算机上更改现有编译器的后端,以创建在目标计算机上运行的输出。然后,您将拥有一个不错的完整编译器,并可以在目标计算机上工作。对此最流行的是C语言,因为许多现有的编译器都具有可插拔的后端,可以将其交换出来。

鲜为人知的事实是,GNU C ++编译器具有仅使用C子集的实现。原因是通常很容易为新的目标机器找到C编译器,从而使您可以从中构建完整的GNU C ++编译器。现在,您已经引导自己在目标计算机上安装了C ++编译器。


14

通常,您需要首先对编译器进行有效的工作(如果是原始的),然后就可以开始考虑使其自托管了。实际上,在某些语言中,这被认为是重要的里程碑。

从“单声道”的记忆中,很可能他们需要在反思中添加一些内容才能使其正常工作:单声道团队不断指出,有些事情根本无法实现Reflection.Emit;当然,MS团队可能会证明他们错了。

这有一些真正的优点:对于初学者来说,这是一个相当不错的单元测试!而且您只需要担心一种语言(即C#专家可能对C ++不太了解;但是现在您可以修复C#编译器)。但是我想知道这里是否没有专业的骄傲:他们只是希望它能够自我托管。

虽然还不是一个编译器,但是我最近一直在研究一个自托管的系统。代码生成器用于生成代码生成器...因此,如果架构发生更改,我只需在自身上运行它即可:新版本。如果有错误,我将返回到早期版本,然后重试。非常方便,而且非常易于维护。


更新1

我刚刚在PDC上观看了Anders的视频,并且(大约一个小时)他确实给出了更多有效的理由-所有关于编译器即服务的信息。仅作记录。


4

这是转储(实际上很难搜索的主题):

这也是PyPyRubinius的想法:

(我认为这也可能适用于Forth,但我对Forth一无所知。)


指向与Smalltalk相关的文章的第一个链接当前指向的页面没有明显有用的即时信息。
nbro

1

GNU Gda Ada编译器GNAT需要完全构建Ada编译器。将其移植到没有可用的GNAT二进制文件的平台时,这可能会很痛苦。


1
我不明白为什么?没有规则,您必须多次引导(就像每个新平台一样),也可以与当前平台交叉编译。
Marco van de Voort,2012年

1

实际上,由于上​​述原因,大多数编译器都是以其编译语言编写的。

第一个引导程序编译器通常是用C,C ++或Assembly编写的。


1

Mono项目的C#编译器已经“自我托管”很长时间了,这意味着它是用C#本身编写的。

我知道的是,编译器是作为纯C代码启动的,但是一旦实现了ECMA的“基本”功能,它们便开始用C#重写编译器。

我不知道用相同语言编写编译器的好处,但是我确信它至少与语言本身可以提供的功能有关(例如,C不支持面向对象的编程) 。

您可以在此处找到更多信息。


1

我自己编写了SLIC(用于实现编译器的语言系统)。然后手工将其编译为汇编。SLIC有很多东西,因为它是包含五个子语言的单个编译器:

  • SYNTAX Parser编程语言PPL
  • 基于GENERATOR LISP 2的树爬式PSEUDO代码生成语言
  • ISO依序,PSEUDO代码,优化语言
  • PSEUDO Macro类似于汇编代码生成语言。
  • MACHOP汇编机器指令定义语言。

SLIC受CWIC(编写和实现编译器的编译器)的启发。与大多数编译器开发包不同,SLIC和CWIC专门针对特定领域的语言处理代码生成。SLIC扩展了CWIC的代码生成功能,添加了ISO,PSEUDO和MACHOP子语言,将目标机器的详细信息与树状生成器语言分开。

LISP 2树和列表

基于LISP 2的生成器语言的动态内存管理系统是关键组件。列表用方括号括起来的语言表示,其组成部分用逗号分隔,即三个元素[a,b,c]列表。

树木:

     ADD
    /   \
  MPY     3
 /   \
5     x

由其第一个条目是节点对象的列表表示:

[ADD,[MPY,5,x],3]

通常显示树,并且在分支之前将节点分开:

ADD[MPY[5,x],3]

基于LISP 2的生成器功能无法解析

生成器函数是一组命名的(unparse)=> action>对...

<NAME>(<unparse>)=><action>;
      (<unparse>)=><action>;
            ...
      (<unparse>)=><action>;

非解析表达式是匹配树模式和/或对象类型的测试,这些模式将树模式和/或对象类型分解开,并将这些部分分配给要由其过程操作处理的局部变量。有点像带有不同参数类型的重载函数。除了()=> ...以外,尝试按编码顺序进行测试。第一个成功的未解析执行其相应的动作。未解析的表达式正在反汇编测试。ADD [x,y]匹配两个分支ADD树,将其分支分配给局部变量x和y。该动作可以是简单的表达式,也可以是.BEGIN ... .END有界代码块。我今天将使用c样式{...}块。树匹配[],未解析规则可能会调用生成器,将返回的结果传递给操作:

expr_gen(ADD[expr_gen(x),expr_gen(y)])=> x+y;

具体来说,上面的expr_gen unparse与两个分支的ADD树匹配。在测试模式中,将使用该分支调用放置在树枝中的单个参数生成器。虽然它的参数列表是分配给返回对象的局部变量。在unparse上方指定两个分支是ADD树反汇编,将每个分支递归地压到expr_gen。左分支返回放置在局部变量x中。同样,右分支与y返回对象一起传递给expr_gen。以上内容可能是数字表达式评估程序的一部分。上面有一些称为矢量的快捷功能,而不是节点字符串,可以将节点的矢量与相应动作的矢量一起使用:

expr_gen(#node[expr_gen(x),expr_gen(y)])=> #action;

  node:   ADD, SUB, MPY, DIV;
  action: x+y, x-y, x*y, x/y;

        (NUMBER(x))=> x;
        (SYMBOL(x))=> val:(x);

上面的更完整的表达式评估器将expr_gen的左分支的返回值分配给x,将右分支的返回值分配给y。返回对x和y执行的相应动作向量。最后的unparse => action对匹配数字和符号对象。

符号和符号属性

符号可能具有命名属性。val:(x)访问x中包含的符号对象的val属性。通用符号表堆栈是SLIC的一部分。可以推送和弹出SYMBOL表,以提供功能的本地符号。新创建的符号在顶部符号表中分类。符号查找从顶部表开始向后向下搜索符号表堆栈。

生成机器独立代码

SLIC的生成器语言生成PSEUDO指令对象,并将它们附加到部分代码列表中。.FLUSH导致其PSEUDO代码列表运行,从而从列表中删除每个PSEUDO指令并对其进行调用。执行后,将释放PSEUDO对象存储器。PSEUDO和GENERATOR动作的过程主体除输出外,基本上是相同的语言。PSEUDO旨在充当汇编宏,提供与机器无关的代码序列。它们提供了特定目标机器与树搜寻器生成器语言的分离。PSEUDO调用MACHOP函数以输出机器代码。MACHOP用于定义汇编伪操作(如dc,定义常量等)和机器指令或使用矢量入口的一系列格式化指令。他们只是将其参数转换为组成指令的一系列位字段。MACHOP调用旨在看起来像汇编,并为汇编在清单中显示时提供字段的打印格式。在示例代码中,我使用的c样式注释可以轻松添加,但不是原始语言。MACHOP正在将代码生成到位可寻址存储器中。SLIC链接器处理编译器的输出。使用矢量输入的DEC-10用户模式指令的MACHOP:MACHOP正在将代码生成到位可寻址存储器中。SLIC链接器处理编译器的输出。使用矢量输入的DEC-10用户模式指令的MACHOP:MACHOP正在将代码生成到位可寻址存储器中。SLIC链接器处理编译器的输出。使用矢量输入的DEC-10用户模式指令的MACHOP:

.MACHOP #opnm register,@indirect offset (index): // Instruction's parameters.
.MORG 36, O(18): $/36; // Align to 36 bit boundary print format: 18 bit octal $/36
O(9):  #opcd;          // Op code 9 bit octal print out
 (4):  register;       // 4 bit register field appended print
 (1):  indirect;       // 1 bit appended print
 (4):  index;          // 4 bit index register appended print
O(18): if (#opcd&&3==1) offset // immediate mode use value else
       else offset/36;         // memory address divide by 36
                               // to get word address.
// Vectored entry opcode table:
#opnm := MOVE, MOVEI, MOVEM, MOVES, MOVS, MOVSI, MOVSM, MOVSS,
         MOVN, MOVNI, MOVNM, MOVNS, MOVM, MOVMI, MOVMM, MOVMS,
         IMUL, IMULI, IMULM, IMULB, MUL,  MULI,  MULM,  MULB,
                           ...
         TDO,  TSO,   TDOE,  TSOE,  TDOA, TSOA,  TDON,  TSON;
// corresponding opcode value:
#opcd := 0O200, 0O201, 0O202, 0O203, 0O204, 0O205, 0O206, 0O207,
         0O210, 0O211, 0O212, 0O213, 0O214, 0O215, 0O216, 0O217,
         0O220, 0O221, 0O222, 0O223, 0O224, 0O225, 0O226, 0O227,
                           ...
         0O670, 0O671, 0O672, 0O673, 0O674, 0O675, 0O676, 0O677;

.MORG 36,O(18):$ / 36; 将位置对齐到36位边界,以八进制打印18位的位置$ / 36字地址。将9位opcd,4位寄存器,间接位和4位索引寄存器组合并打印,就好像一个18位字段一样。输出18位地址/ 36或立即数,并以八进制打印。用r1 = 1和r2 = 2打印的MOVEI示例:

400020 201082 000005            MOVEI r1,5(r2)

使用编译器程序集选项,您可以在编译列表中获取生成的程序集代码。

链接在一起

SLIC链接器作为处理链接和符号解析的库提供。尽管必须为目标计算机编写目标特定的输出负载文件格式,并与链接程序库库链接。

生成器语言能够将树写入文件并读取它们,从而可以实现多遍编译器。

短暂的代码生成和起源

我首先检查了代码生成过程,以确保可以理解SLIC是真正的编译器编译器。SLIC受1960年代后期在Systems Development Corporation开发的CWIC(编写和实现编译器的编译器)的启发。CWIC仅使用SYNTAX和GENERATOR语言从GENERATOR语言中生成数字字节代码。字节码被放置或植入(在CWIC文档中使用的术语)到与命名节关联的内存缓冲区中,并由.FLUSH语句写出。可从ACM档案中获取有关CWIC的ACM论文。

成功实施主要的编程语言

在1970年代后期,使用SLIC编写了COBOL交叉编译器。在大约3个月内完成,主要由一个程序员完成。我根据需要与程序员进行了一些合作。另一位程序员为目标TI-990微型计算机编写了运行时库和MACHOP。与以汇编语言编写的DEC-10本机COBOL编译器相比,该COBOL编译器每秒可编译更多行。

然后更多地是关于编译器的讨论

从头开始编写编译器的很大一部分是运行时库。您需要一个符号表。您需要输入和输出。动态内存管理等。为编译器编写运行时库,然后再编写编译器,可以轻松完成更多工作。但是对于SLIC,运行时库对于SLIC中开发的所有编译器都是通用的。请注意,有两个运行时库。一种用于语言的目标计算机(例如COBOL)。另一个是编译器编译器的运行时库。

我想我已经确定这些不是解析器生成器。因此,现在只要稍微了解一下后端,我就可以解释解析器编程语言。

解析器编程语言

解析器使用以简单方程式形式编写的公式编写。

<name> <formula type operator> <expression> ;

最低级别的语言元素是字符。令牌由语言字符的子集形成。字符类用于命名和定义那些字符子集。字符类定义运算符是冒号(:)字符。属于类成员的字符编码在定义的右侧。可打印字符用撇号'括起来。非印刷和特殊字符可以用数字序号表示。类成员由替代项分隔。操作员。类公式以分号结尾。字符类可以包括以前定义的类:

/*  Character Class Formula                                    class_mask */
bin: '0'|'1';                                                // 0b00000010
oct: bin|'2'|'3'|'4'|'5'|'6'|'7';                            // 0b00000110
dgt: oct|'8'|'9';                                            // 0b00001110
hex: dgt|'A'|'B'|'C'|'D'|'E'|'F'|'a'|'b'|'c'|'d'|'e'|'f';    // 0b00011110
upr:  'A'|'B'|'C'|'D'|'E'|'F'|'G'|'H'|'I'|'J'|'K'|'L'|'M'|
      'N'|'O'|'P'|'Q'|'R'|'S'|'T'|'U'|'V'|'W'|'X'|'Y'|'Z';   // 0b00100000
lwr:  'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i'|'j'|'k'|'l'|'m'|
      'n'|'o'|'p'|'q'|'r'|'s'|'t'|'u'|'v'|'w'|'x'|'y'|'z';   // 0b01000000
alpha:  upr|lwr;                                             // 0b01100000
alphanum: alpha|dgt;                                         // 0b01101110

skip_class 0b00000001是预定义的,但可能在定义skip_class方面有些困难。

概括来说:字符类是替代列表,只能是字符常量,字符序数或先前定义的字符类。当我实现字符类时:类公式被分配了一个类位掩码。(在上面的注释中显示)具有任何字符文字或序数的任何类公式都会导致分配一个类位。通过将所包括的类的类掩码与分配的位(如果有)一起使用来制作掩码。从字符类创建一个类表。由字符序号索引的条目包含指示字符的类成员资格的位。类测试是内联完成的。IA-86代码示例(在eax中使用字符的序数)说明了类测试:

test    byte ptr [eax+_classmap],dgt

随后是:

jne      <success>

要么

je       <failure>

之所以使用IA-86指令代码示例,是因为我认为IA-86指令在当今已广为人知。评估其类掩码的类名称与由ordinal(在eax中)字符索引的类表进行非破坏性AND运算。非零结果表示类成员身份。(EAX被清零,但包含字符的al(EAX的低8位)除外。

在这些旧的编译器中,令牌有些不同。关键字未解释为标记。它们只是由解析器语言中带引号的字符串常量匹配。带引号的字符串通常不保留。可以使用修饰符。+使字符串保持匹配。(即+'-'与-字符匹配,成功时将保留该字符)。,操作(即'E')将字符串插入令牌中。空格由令牌公式处理,跳过前导SKIP_CLASS字符,直到进行第​​一个匹配为止。请注意,显式的skip_class字符匹配将停止跳过,从而允许令牌以skip_class字符开头。字符串令牌公式跳过与单引号字母或双引号字符串匹配的前导skip_class字符。感兴趣的是在带引号的字符串中匹配“”字符:

string .. (''' .ANY ''' | '"' $(-"""" .ANY | """""","""") '"') MAKSTR[];

第一种选择匹配任何单引号引起来的字符。正确的选择与双引号引起来的字符串匹配,该字符串可能包含使用两个“字符一起表示一个”字符的双引号字符。此公式定义了在其自己的定义中使用的字符串。内部右选项'“'$(-”“”“ .ANY |”“”“”,“”“”)'“'与双引号引起来的字符串匹配。我们可以使用单个带引号的字符来匹配双引号字符。但是,如果希望使用“字符”,则在双引号字符串内必须使用两个“”字符。例如,在左内侧替代中,匹配除引号之外的任何字符:

-"""" .ANY

否定前视-“”“”用于表示成功(不匹配“字符”)然后匹配.ANY字符(不能是“字符”,因为-“”“”消除了这种可能性)。正确的选择是-匹配一个字符的“”“”“,失败是正确的选择:

"""""",""""

尝试匹配两个“字符,并用一个双精度”使用“,”“”“将其替换为插入单个”字符。两个不包含结束字符串引号字符的内部替代方案都匹配,并调用MAKSTR []创建一个字符串对象。序列,成功时循环,使用运算符匹配序列。令牌公式跳过前导跳过类字符(空格)。一旦进行第一个匹配,skip_class跳过被禁用。我们可以使用[]调用其他语言编写的函数。提供了[],MAKBIN [],MAKOCT [],MAKHEX [],MAKFLOAT []和MAKINT []库函数,这些函数将匹配的令牌字符串转换为类型化的对象。以下数字公式说明了相当复杂的令牌识别:

number .. "0B" bin $bin MAKBIN[]        // binary integer
         |"0O" oct $oct MAKOCT[]        // octal integer
         |("0H"|"0X") hex $hex MAKHEX[] // hexadecimal integer
// look for decimal number determining if integer or floating point.
         | ('+'|+'-'|--)                // only - matters
           dgt $dgt                     // integer part
           ( +'.' $dgt                  // fractional part?
              ((+'E'|'e','E')           // exponent  part
               ('+'|+'-'|--)            // Only negative matters
               dgt(dgt(dgt|--)|--)|--)  // 1 2 or 3 digit exponent
             MAKFLOAT[] )               // floating point
           MAKINT[];                    // decimal integer

上面的数字令牌公式可识别整数和浮点数。-替代方案总是成功的。可以在计算中使用数值对象。公式成功后,令牌对象将被推入解析堆栈。(+'E'|'e','E')中的指数前导很有趣。我们希望对MAKEFLOAT []始终使用大写E。但是我们允许使用小写的'e'替换为'E'。

您可能已经注意到字符类和令牌公式的一致性。解析公式继续增加了回溯替代方案和树构造运算符。表达式级别内不得混入回溯和非回溯替代运算符。您可能没有(a | b \ c)混合非回溯| 与\回溯替代。(a \ b \ c),(a | b | c)和((a | b)\ c)有效。\回溯替代方案将在尝试左替代方案之前保存解析状态,并且在失败时将在尝试右替代方案之前恢复解析状态。在一系列备选方案中,第一个成功的备选方案满足了小组的要求。没有尝试其他替代方法。分解和分组提供了连续的高级解析。回溯替代方案将在尝试左替代方案之前创建解析的保存状态。当解析可能部分匹配然后失败时,需要回溯:

(a b | c d)\ e

在上面,如果返回失败,则尝试使用替代cd。如果然后c返回失败,将尝试回溯替代方法。如果a成功而b失败,则解析回溯并尝试e。同样,失败的c成功和b失败,解析被回溯并采用替代e。回溯不限于公式内。如果任何解析公式在任何时候都进行部分匹配,然后失败,则解析将重置为顶部回溯,并采用其他方法。如果已输出代码以检测到回溯已创建,则可能会发生编译失败。在开始编译之前设置了回溯。返回失败或回溯到失败是编译器失败。回溯是堆叠的。我们可以使用负数和正数吗?在不提高解析度的情况下,请先查看/展望操作员进行测试。进行字符串测试是一个先睹为快,只需要保存和重置输入状态即可。展望未来将是一个解析表达式,该表达式在失败之前会进行部分匹配。使用回溯可以实现前瞻性。

解析器语言既不是LL解析器也不是LR解析器。但是是一种用于编写递归体面解析器的编程语言,您可以在其中编写树结构:

:<node name> creates a node object and pushes it onto the node stack.
..           Token formula create token objects and push them onto 
             the parse stack.
!<number>    pops the top node object and top <number> of parstack 
             entries into a list representation of the tree. The 
             tree then pushed onto the parse stack.
+[ ... ]+    creates a list of the parse stack entries created 
             between them:
              '(' +[argument $(',' argument]+ ')'
             could parse an argument list. into a list.

常用的解析示例是算术表达式:

Exp = Term $(('+':ADD|'-':SUB) Term!2); 
Term = Factor $(('*':MPY|'/':DIV) Factor!2);
Factor = ( number
         | id  ( '(' +[Exp $(',' Exp)]+ ')' :FUN!2
               | --)
         | '(' Exp ')" )
         (^' Factor:XPO!2 |--);

使用循环的Exp和Term创建左手树。使用右递归的因子会创建右手树:

d^(x+5)^3-a+b*c => ADD[SUB[EXP[EXP[d,ADD[x,5]],3],a],MPY[b,c]]

              ADD
             /   \
          SUB     MPY
         /   \   /   \
      EXP     a b     c
     /   \
    d     EXP     
         /   \
      ADD     3
     /   \
    x     5

这是cc编译器的一部分,它是带有C样式注释的SLIC的更新版本。函数类型(语法,令牌,字符类,生成器,PSEUDO或MACHOP)由其ID后的初始语法确定。使用这些自上而下的解析器,您可以从定义公式的程序开始:

program = $((declaration            // A program is a sequence of
                                    // declarations terminated by
            |.EOF .STOP)            // End Of File finish & stop compile
           \                        // Backtrack: .EOF failed or
                                    // declaration long-failed.
             (ERRORX["?Error?"]     // report unknown error
                                    // flagging furthest parse point.
              $(-';' (.ANY          // find a ';'. skiping .ANY
                     | .STOP))      // character: .ANY fails on end of file
                                    // so .STOP ends the compile.
                                    // (-';') failing breaks loop.
              ';'));                // Match ';' and continue

declaration =  "#" directive                // Compiler directive.
             | comment                      // skips comment text
             | global        DECLAR[*1]     // Global linkage
             |(id                           // functions starting with an id:
                ( formula    PARSER[*1]     // Parsing formula
                | sequencer  GENERATOR[*1]  // Code generator
                | optimizer  ISO[*1]        // Optimizer
                | pseudo_op  PRODUCTION[*1] // Pseudo instruction
                | emitor_op  MACHOP[*1]     // Machine instruction
                )        // All the above start with an identifier
              \ (ERRORX["Syntax error."]
                 garbol);                    // skip over error.

//请注意,如何在创建树时将id分解,并在以后合并。

formula =   ("==" syntax  :BCKTRAK   // backtrack grammar formula
            |'='  syntax  :SYNTAX    // grammar formula.
            |':'  chclass :CLASS     // character class define
            |".." token   :TOKEN     // token formula
              )';' !2                // Combine node name with id 
                                     // parsed in calling declaration 
                                     // formula and tree produced
                                     // by the called syntax, token
                                     // or character class formula.
                $(-(.NL |"/*") (.ANY|.STOP)); Comment ; to line separator?

chclass = +[ letter $('|' letter) ]+;// a simple list of character codes
                                     // except 
letter  = char | number | id;        // when including another class

syntax  = seq ('|' alt1|'\' alt2 |--);

alt1    = seq:ALT!2 ('|' alt1|--);  Non-backtrack alternative sequence.

alt2    = seq:BKTK!2 ('\' alt2|--); backtrack alternative sequence

seq     = +[oper $oper]+;

oper    = test | action | '(' syntax ')' | comment; 

test    = string | id ('[' (arg_list| ,NILL) ']':GENCALL!2|.EMPTY);

action  = ':' id:NODE!1
        | '!' number:MAKTREE!1
        | "+["  seq "]+" :MAKLST!1;

//     C style comments
comment  = "//" $(-.NL .ANY)
         | "/*" $(-"*/" .ANY) "*/";

值得注意的是解析器语言如何处理注释和错误恢复。

我想我已经回答了这个问题。cc语言本身就是SLIC继任者的重要组成部分。尚无编译器。但是我可以手工将其编译为汇编代码,裸露的asm c或c ++函数。


0

是的,您可以使用该语言编写一种语言的编译器。不,您不需要该语言的第一个编译器即可启动。

您需要引导的是该语言的实现。可以是编译器也可以是解释器。

从历史上看,语言通常被认为是解释性语言或编译语言。口译员仅为前者编写,而编译器仅为后者编写。因此,通常,如果要为某种语言编写编译器,则第一个编译器将以其他某种语言编写以引导它,然后(可选)将为主题语言重新编写该编译器。但是可以选择用另一种语言编写口译员。

这不只是理论上的。我碰巧目前正在自己​​做。我正在开发自己开发的Salmon语言编译器。我首先用C创建了Salmon编译器,现在我用Salmon编写了该编译器,因此,即使没有用任何其他语言编写的Salmon编译器,我也可以使Salmon编译器正常工作。


-1

也许你可以写一个BNF描述BNF。


4
您确实可以(也没有那么困难),但是它唯一的实际应用是在解析器生成器中。
Daniel Spiewak

确实,我使用这种方法来生成LIME解析器生成器。通过简单的递归下降语法分析器,对语法进行了严格的简化表格式表示。然后,LIME为语法的语言生成一个解析器,然后使用该解析器读取某人真正有兴趣为其生成解析器的语法。这意味着我不必知道如何写我刚刚写的东西。感觉就像魔术。
伊恩

实际上,您无法做到,因为BNF无法描述自己。您需要一个变体,例如yacc中使用的变体,其中不带非终结符。
洛恩侯爵,

1
您无法使用bnf定义bnf,因为无法识别<>。EBNF通过引用该语言的常量字符串标记来解决该问题。
GK
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.