Dón*_*nal 196 compiler-construction bootstrapping
直观地说,似乎语言的编译器Foo本身不能用Foo编写.更具体地说,语言的第一个编译器Foo不能用Foo编写,但可以编写任何后续的编译器Foo.
但这是真的吗?我对一种语言的阅读非常模糊,这种语言的第一个编译器是用"本身"编写的.这是可能的,如果是这样的话怎么样?
Dan*_*wak 225
这称为"自举".您必须首先使用其他语言(通常是Java或C)为您的语言构建编译器(或解释器).完成后,您可以用Foo语言编写新版本的编译器.您使用第一个引导编译器来编译编译器,然后使用此编译的编译器来编译其他所有内容(包括其自身的未来版本).
大多数语言确实以这种方式创建,部分原因是语言设计者喜欢使用他们正在创建的语言,并且因为非平凡的编译器通常作为语言"完整"的有用基准.
这方面的一个例子是Scala.它的第一个编译器是由Martin Odersky的实验性语言Pizza创建的.从2.0版开始,编译器完全在Scala中重写.从那时起,旧的Pizza编译器可能被完全丢弃,因为新的Scala编译器可以用于编译自己以用于将来的迭代.
Fed*_*oni 47
为先前的答案添加好奇心.
这里引用了Linux From Scratch手册,其中一个开始从源代码开始构建GCC编译器.(Linux From Scratch是一种安装Linux的方法,与安装发行版完全不同,因为你必须编译目标系统的每一个二进制文件.)
Run Code Online (Sandbox Code Playgroud)make bootstrap'bootstrap'目标不只是编译GCC,而是编译几次.它使用第一轮编译的程序第二次编译自己,然后第三次编译.然后它比较这些第二和第三个编译,以确保它可以完美地再现自己.这也意味着它被正确编译.
使用'bootstrap'目标的动机是,用于构建目标系统的工具链的编译器可能没有与目标编译器完全相同的版本.以这种方式进行,确保在目标系统中获得可以自己编译的编译器.
Aar*_*lla 43
当您为C编写第一个编译器时,可以用其他语言编写它.现在,你有一个C编译器,比如汇编器.最终,您将到达必须解析字符串的位置,特别是转义序列.您将编写代码以转换\n为十进制代码10(以及\r13等)的字符.
在编译器准备好之后,您将开始在C中重新实现它.此过程称为" bootstrapping" ".
字符串解析代码将变为:
...
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".
如果你编译它并将其作为二进制文件传播,那么通过查看源代码就无法找到病毒.
这个想法的原始来源:http://cm.bell-labs.com/who/ken/trust.html
Phi*_*ght 18
你不能自己编写编译器,因为你没有什么可以编译你的起始源代码.解决这个问题有两种方法.
最不受欢迎的是以下内容.您在汇编程序(yuck)中编写了一个最小的编译器,用于最小的语言集,然后使用该编译器来实现该语言的额外功能.建立自己的方式,直到你有一个编译器具有自己的所有语言功能.一个痛苦的过程通常只在你别无选择时才会完成.
首选方法是使用交叉编译器.您可以更改其他计算机上现有编译器的后端,以创建在目标计算机上运行的输出.然后你有一个很好的完整编译器并在目标机器上工作.最受欢迎的是C语言,因为有很多现有的编译器具有可插拔的可插拔端口.
一个鲜为人知的事实是GNU C++编译器具有仅使用C子集的实现.原因是通常很容易为新的目标机器找到C编译器,然后允许您从中构建完整的GNU C++编译器.你现在已经开始在目标机器上使用C++编译器了.
Mar*_*ell 14
通常,您需要首先使用编译器的工作(如果是主要的) - 然后您可以开始考虑使其自托管.这实际上被认为是一些语言中的一个重要里程碑.
从我记得的"单声道"中,很可能他们需要添加一些东西来反思才能让它发挥作用:单声道团队一直指出有些事情是不可能的Reflection.Emit; 当然,MS团队可能会证明他们错了.
这有一些真正的优势:对于初学者来说,这是一个相当不错的单元测试!而且你只需要担心一种语言(即C#专家可能不太了解C++;但现在你可以修复C#编译器).但我想知道在这里是否有一定程度的职业自豪感:他们只是希望它能够自我托管.
不是一个编译器,但我最近一直在研究一个自托管的系统; 代码生成器用于生成代码生成器...所以如果模式更改我只是自己运行它:新版本.如果有错误,我只需返回早期版本再试一次.非常方便,而且非常容易维护.
我刚看过PDC上的Anders 视频,(大约一个小时后)他确实给出了一些更有效的理由 - 所有关于编译器作为服务.仅供记录.
小智 7
我自己编写了 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>;
未解析表达式是匹配树模式和/或对象类型的测试,将它们分开并将这些部分分配给局部变量以由其过程操作进行处理。有点像采用不同参数类型的重载函数。除了 ()=> ... 测试按编码顺序尝试。第一个成功的解解析执行其相应的操作。未解析表达式是反汇编测试。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 中。同样,右分支传递给 expr_gen,y 作为返回对象。以上可以是数值表达式求值器的一部分。上面有称为向量的快捷功能,而不是节点字符串,节点向量可以与相应操作的向量一起使用:
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 旨在充当汇编宏,提供与机器无关的代码序列化。它们提供了一种将特定目标机器从树爬行生成器语言中分离出来的方法。PSEUDO 调用 MACHOP 函数来输出机器代码。MACHOP 用于定义汇编伪操作(如 dc、定义常量等)和机器指令或使用向量条目的一系列类似格式的指令。它们只是将参数转换为构成指令的一系列位字段。MACHOP 调用旨在看起来像程序集,并在编译列表中显示程序集时提供字段的打印格式。在示例代码中,我使用了 c 风格的注释,可以轻松添加该注释,但原始语言中没有。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 或立即值以八进制输出和打印。MOVEI 示例打印出 r1 = 1 且 r2=2:
400020 201082 000005            MOVEI r1,5(r2)
使用编译器汇编选项,您可以在编译列表中获得生成的汇编代码。
SLIC 链接器作为处理链接和符号解析的库提供。但必须为目标机器编写特定于目标的输出加载文件格式并与链接器库链接。
生成器语言能够将树写入文件并读取它们,从而允许实现多遍编译器。
我首先回顾了代码生成,以确保人们理解 SLIC 是一个真正的编译器。SLIC 的灵感来自于 20 世纪 60 年代末 Systems Development Corporation 开发的 CWIC(用于编写和实现编译器的编译器)。CWIC 仅具有从 GENERATOR 语言生成数字字节代码的 SYNTAX 和 GENERATOR 语言。字节代码被放置或种植(CWIC 文档中使用的术语)到与命名节相关的内存缓冲区中,并通过 .FLUSH 语句写出。ACM 档案中提供了有关 CWIC 的 ACM 论文。
在 20 世纪 70 年代末,SLIC 被用来编写 COBOL 交叉编译器。大部分由一名程序员在大约 3 个月内完成。我根据需要与程序员一起工作了一些。另一位程序员为目标 TI-990 迷你计算机编写了运行时库和 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 中)索引的类表进行非破坏性 AND 运算。非零结果表示类成员资格。(除了包含该字符的 al(EAX 的低 8 位)之外,EAX 均为零)。
这些旧编译器中的令牌有点不同。关键词没有被解释为标记。它们只是通过解析器语言中带引号的字符串常量进行匹配。通常不保留引用的字符串。可以使用修饰符。A + 保持字符串匹配。(即 +'-' 匹配 - 字符,成功时保留该字符) , 操作(即 'E')将字符串插入到标记中。空白由令牌公式处理,跳过前导 SKIP_CLASS 字符,直到进行第一个匹配。请注意,显式的skip_class字符匹配将停止跳过,允许令牌以skip_class字符开头。字符串标记公式会跳过与单引号 quitdd 字符或双引号字符串匹配的前导 Skip_class 字符。有趣的是匹配 " 带引号的字符串中的 " 字符:
string .. (''' .ANY ''' | '"' $(-"""" .ANY | """""","""") '"') MAKSTR[];
第一个选项匹配任何单引号字符。正确的替代方案匹配双引号引起来的字符串,该字符串可能包含双引号字符,使用两个 " 字符一起表示单个 " 字符。该公式定义了其自身定义中使用的字符串。内部右替代 '"' $(-"""" .ANY | """""","""") '"' 匹配双引号引起来的字符串。我们可以使用单个 ' 引号字符来匹配双引号 " 字符。但是,在双 " 引号字符串中,如果我们希望使用 " 字符,则必须使用两个 " 字符来获取一个字符。例如,在内部左侧替代中,匹配除引号之外的任何字符:
-"""" .ANY
使用负向前查看 -"""" 当成功时(不匹配 " 字符)则匹配 .ANY 字符(不能是 " 字符,因为 -"""" 消除了这种可能性)。正确的选择是采用 -"""" 匹配 " 字符,失败是正确的选择:
"""""",""""
尝试匹配两个 " 字符,使用 ,"""" 插入单个 " 字符,将它们替换为单个双 " 字符。匹配失败结束字符串引号字符的两个内部替代方案,并调用 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 失败,则解析将被回溯并尝试 e。同样,a 失败 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) "*/";
值得注意的是解析器语言如何处理注释和错误恢复。
我想我已经回答了这个问题。在这里编写了 SLIC 的后继者 cc 语言本身的大部分内容。目前还没有适合它的编译器。但我可以手动将其编译成汇编代码、裸 asm c 或 c++ 函数。