直观地说,似乎语言的编译器Foo
本身不能用 Foo 编写。更具体地说,语言的第一个编译器Foo
不能用 Foo 编写,但任何后续编译器都可以用Foo
.
但这真的是真的吗?我对阅读第一个编译器是用“自身”编写的语言有一些非常模糊的回忆。这可能吗,如果可以,怎么办?
直观地说,似乎语言的编译器Foo
本身不能用 Foo 编写。更具体地说,语言的第一个编译器Foo
不能用 Foo 编写,但任何后续编译器都可以用Foo
.
但这真的是真的吗?我对阅读第一个编译器是用“自身”编写的语言有一些非常模糊的回忆。这可能吗,如果可以,怎么办?
这称为“引导”。您必须首先用其他语言(通常是 Java 或 C)为您的语言构建编译器(或解释器)。完成后,您可以使用 Foo 语言编写新版本的编译器。你使用第一个引导编译器来编译编译器,然后使用这个编译的编译器来编译其他所有东西(包括它自己的未来版本)。
大多数语言确实是以这种方式创建的,部分原因是语言设计者喜欢使用他们正在创建的语言,还因为非平凡的编译器通常可以作为语言“完整”程度的有用基准。
Scala就是一个例子。它的第一个编译器是用 Martin Odersky 的实验性语言 Pizza 创建的。从 2.0 版开始,编译器完全用 Scala 重写。从那时起,旧的 Pizza 编译器可能会被完全丢弃,因为新的 Scala 编译器可以用于为未来的迭代进行编译。
我记得听过一个软件工程广播播客,其中 Dick Gabriel 谈到通过在纸上用 LISP 编写一个准系统版本并将其手工组装成机器代码来引导原始 LISP 解释器。从那时起,其余的 LISP 功能都是用 LISP 编写和解释的。
当你为 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 对于我们想要用我们的病毒替换的功能是相同的。例如,它可能是来自 Linux 内核的源文件“login.c”中的函数“login”。除了普通密码外,我们可以将它替换为接受 root 帐户密码“joshua”的版本。
如果您将其编译并作为二进制文件传播,则无法通过查看源代码来找到病毒。
想法的原始出处:https ://web.archive.org/web/20070714062657/http://www.acm.org/classics/sep95/
对以前的答案增加好奇心。
这是Linux From Scratch手册中的一段话,在开始从源代码构建 GCC 编译器的步骤中。(Linux From Scratch 是一种安装 Linux 的方法,它与安装发行版完全不同,因为您必须编译目标系统的每一个二进制文件。)
make bootstrap
'bootstrap' 目标不只是编译 GCC,而是多次编译。它使用第一轮编译的程序进行第二次编译,然后再次编译第三次。然后它会比较这些第二次和第三次编译,以确保它可以完美地复制自己。这也意味着它被正确编译。
使用“引导”目标的动机是,用于构建目标系统工具链的编译器可能与目标编译器的版本不同。以这种方式进行,肯定会在目标系统中获得一个可以自行编译的编译器。
你不能自己编写编译器,因为你没有任何东西可以用来编译你的起始源代码。有两种方法可以解决这个问题。
最不受欢迎的是以下。您在汇编程序(yuck)中为最小的语言集编写了一个最小的编译器,然后使用该编译器来实现该语言的额外功能。建立自己的方式,直到您拥有一个具有所有语言功能的编译器。一个痛苦的过程,通常只有在您别无选择时才会完成。
首选方法是使用交叉编译器。您更改另一台机器上现有编译器的后端,以创建在目标机器上运行的输出。然后你有一个很好的完整编译器并在目标机器上工作。最流行的是 C 语言,因为有很多现有的编译器具有可替换的可插拔后端。
一个鲜为人知的事实是 GNU C++ 编译器有一个只使用 C 子集的实现。原因是通常很容易为新的目标机器找到 C 编译器,然后您可以从中构建完整的 GNU C++ 编译器。您现在已经在目标机器上安装了 C++ 编译器。
通常,您需要首先对编译器进行工作(如果是原始的)切割 - 然后您可以开始考虑使其自托管。在某些语言中,这实际上被认为是一个重要的里程碑。
根据我对“mono”的记忆,他们可能需要在反射中添加一些东西才能使其正常工作:mono 团队一直指出有些事情根本不可能用Reflection.Emit
; 当然,MS 团队可能会证明他们错了。
这有几个真正的优点:对于初学者来说,这是一个相当不错的单元测试!而且您只需要担心一种语言(即 C# 专家可能不太了解 C++;但现在您可以修复 C# 编译器)。但我想知道这里的工作是否缺乏职业自豪感:他们只是希望它是自托管的。
不是一个编译器,但我最近一直在开发一个自托管的系统;代码生成器用于生成代码生成器......所以如果架构发生变化,我只需在其自身上运行它:新版本。如果有错误,我只是回到早期版本并重试。很方便,也很容易维护。
我刚刚在 PDC 观看了 Anders 的这段视频,并且(大约一个小时后)他确实给出了一些更有效的理由——关于编译器即服务。只是为了记录。
我自己编写了 SLIC(用于实现编译器的语言系统)。然后手工编译成程序集。SLIC 有很多功能,因为它是五种子语言的单一编译器:
SLIC 的灵感来自 CWIC(编写和实现编译器的编译器)。与大多数编译器开发包不同,SLIC 和 CWIC 使用专门的、特定于领域的语言来解决代码生成问题。SLIC 扩展了 CWIC 代码生成,添加了 ISO、PSEUDO 和 MACHOP 子语言,将目标机器的细节从树爬行生成器语言中分离出来。
基于 LISP 2 的生成器语言的动态内存管理系统是一个关键组件。列表用方括号括起来的语言表示,其组件用逗号分隔,即三元素 [a,b,c] 列表。
树木:
ADD
/ \
MPY 3
/ \
5 x
由第一个条目是节点对象的列表表示:
[ADD,[MPY,5,x],3]
树通常以节点分开显示在分支之前:
ADD[MPY[5,x],3]
生成器函数是一组命名的 (unparse)=>action> 对 ...
<NAME>(<unparse>)=><action>;
(<unparse>)=><action>;
...
(<unparse>)=><action>;
非解析表达式是匹配树模式和/或对象类型的测试,将它们分开并将这些部分分配给局部变量以通过其程序操作进行处理。有点像采用不同参数类型的重载函数。除了 ()=> ... 测试按编码顺序进行尝试。第一个成功的 unparse 执行其相应的操作。unparse 表达式是反汇编测试。ADD[x,y] 匹配两个分支的 ADD 树,将其分支分配给局部变量 x 和 y。动作可能是一个简单的表达式或一个 .BEGIN ... .END 有界代码块。我今天会使用 c style { ... } 块。树匹配、[]、未解析规则可能会调用生成器,将返回的结果传递给操作:
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 mini-COMPUTER 编写了运行时库和 MACHOP。该 COBOL 编译器每秒编译的行数比以汇编语言编写的 DEC-10 本机 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。
总而言之:字符类是只能是字符常量、字符序数或先前定义的字符类的替代列表。当我实现字符类时:类公式被分配了一个类位掩码。(如上面的注释所示)任何具有任何字符文字或序数的类公式都会导致分配一个类位。掩码是通过将包含的类的类掩码与分配的位(如果有)一起进行或运算而制成的。从字符类创建一个类表。由字符序号索引的条目包含指示字符的类成员资格的位。类测试是内联完成的。在 eax 中带有字符序数的 IA-86 代码示例说明了类测试:
test byte ptr [eax+_classmap],dgt
其次是:
jne <success>
或者
je <failure>
使用 IA-86 指令代码示例是因为我认为 IA-86 指令在今天更为广为人知。评估到其类掩码的类名与由字符序数(在 eax 中)索引的类表进行非破坏性与运算。非零结果表示类成员资格。(除了包含字符的 al(EAX 的低 8 位)之外,EAX 为零)。
这些旧编译器中的令牌有点不同。关键词没有被解释为记号。它们只是通过解析器语言中的引用字符串常量进行匹配。通常不保留带引号的字符串。可以使用修饰符。A + 保持字符串匹配。(即 +'-' 匹配 - 字符,成功时保留该字符) , 操作(即,'E')将字符串插入到标记中。空格由令牌公式处理,跳过前导 SKIP_CLASS 字符,直到进行第一次匹配。请注意,明确的 skip_class 字符匹配将停止跳过,从而允许令牌以 skip_class 字符开头。字符串标记公式会跳过与单引号相当dd 字符或双引号字符串匹配的前导skip_class 字符。有趣的是在 " 引用的字符串中匹配 " 字符:
string .. (''' .ANY ''' | '"' $(-"""" .ANY | """""","""") '"') MAKSTR[];
第一个替代匹配任何单引号引号字符。正确的选择匹配一个双引号引用的字符串,该字符串可能包含使用两个 " 字符一起表示单个 " 字符的双引号字符。此公式定义了在其自己的定义中使用的字符串。右内选项 '"' $(-"""" .ANY | """""","""") '"' 匹配双引号引用的字符串。我们可以使用单个 ' 引号字符来匹配双引号 " 字符。但是在双引号 " 字符串中,如果我们希望使用 " 字符,我们必须使用两个 " 字符来获得一个。例如,在左内替代匹配除引号之外的任何字符:
-"""" .ANY
使用负向窥视 -"""" 表示成功时(不匹配 " 字符)然后匹配 .ANY 字符(不能是 " 字符,因为 -"""" 消除了这种可能性)。正确的选择是采用 -"""" 匹配一个 " 字符并且失败是正确的选择:
"""""",""""
尝试匹配两个 " 字符用单个双 " 替换它们,使用 """" 插入 thw 单个 " 字符。匹配结束字符串引号字符失败的两个内部替代方案,并调用 MAKSTR[] 以创建字符串对象。 $序列,成功时循环,运算符用于匹配序列。标记公式跳过前导跳过类字符(空白)。一旦进行第一次匹配,skip_class跳过被禁用。我们可以使用[]调用以其他语言编程的函数。MAKSTR []、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)混合非回溯| withe \回溯替代。(a\b\c)、(a|b|c) 和 ((a|b)\c) 是有效的。\ 回溯替代在尝试其左侧替代之前保存解析状态,并且在失败时在尝试正确替代之前恢复解析状态。在一系列备选方案中,第一个成功的备选方案满足该组。没有尝试进一步的替代方案。分解和分组提供了连续推进的解析。回溯替代在尝试其左替代之前创建解析的保存状态。当解析可能进行部分匹配然后失败时,需要回溯:
(a b | c d)\ e
在上面,如果返回失败,则尝试替代 cd。如果然后 c 返回失败,则将尝试回溯替代方案。如果 a 成功而 b 失败,则将回溯并尝试解析。同样,a 失败 c 成功且 b 失败,则回溯解析并采用替代 e。回溯不限于公式内。如果任何解析公式在任何时候进行部分匹配然后失败,则解析将重置为顶部回溯并采用其替代方案。如果代码已输出,则可能会发生编译失败,因为回溯已创建。在开始编译之前设置回溯。返回失败或回溯到它是编译器失败。回溯是堆叠的。我们可以使用负数和正数?peek/look ahead 运算符在不推进解析的情况下进行测试。字符串测试是一个预览,只需要保存和重置输入状态。向前看将是一个解析表达式,它在失败之前进行部分匹配。使用回溯实现前瞻。
解析器语言既不是 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) "*/";
值得注意的是解析器语言如何处理注释和错误恢复。
我想我已经回答了这个问题。已经编写了 SLIC 继任者的很大一部分,这里是 cc 语言本身。目前还没有编译器。但我可以将它手动编译成汇编代码、裸 asm c 或 c++ 函数。
GNAT 是 GNU Ada 编译器,需要完全构建 Ada 编译器。将其移植到没有现成可用的 GNAT 二进制文件的平台时,这可能会很痛苦。
Mono 项目 C# 编译器已经“自托管”了很长时间,这意味着它是用 C# 本身编写的。
我所知道的是编译器是作为纯 C 代码启动的,但是一旦实现了 ECMA 的“基本”功能,他们就开始用 C# 重写编译器。
我不知道用同一种语言编写编译器的好处,但我确信它至少与语言本身可以提供的特性有关(例如,C 不支持面向对象的编程) .
您可以在此处找到更多信息。
实际上,出于上述原因,大多数编译器都是用它们编译的语言编写的。
第一个引导编译器通常是用 C、C++ 或汇编语言编写的。
是的,您可以为该语言的语言编写编译器。不,您不需要第一个编译器来引导该语言。
您需要引导的是该语言的实现。这可以是编译器或解释器。
从历史上看,语言通常被认为是解释语言或编译语言。解释器只为前者编写,编译器只为后者编写。因此,通常如果要为一种语言编写编译器,第一个编译器会用其他语言编写以引导它,然后,可以选择为主题语言重新编写编译器。但是用另一种语言编写解释器是一种选择。
这不仅仅是理论上的。我碰巧目前正在自己做这件事。我正在为我自己开发的一种语言 Salmon 开发编译器。我首先用 C 创建了一个 Salmon 编译器,现在我正在用 Salmon 编写编译器,这样我就可以让 Salmon 编译器工作,而无需使用任何其他语言编写的 Salmon 编译器。
请注意,从技术上讲,您可以用一种仍然不存在的语言编写编译器。为了做到这一点,您创建了一个解释器,它是原始语言的一种低级语言,它通常是缓慢且无用的,因为它在执行任何操作之前解释语言的每个语句。
如果您阅读它,它确实看起来完全像预期的语言,但它的执行经过了一些过程,该过程在不止一个步骤中将其转换为可执行文件。
这个编译器通常非常慢,因为它使用了一些适用于几乎所有现有语言的通用数学过程,但优点是你下次除了在现有代码上使用生成的编译器之外什么都不做。
这次当然不用解释了。
也许你可以写一个描述 BNF 的 BNF 。